1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrDataType, NDAttrSource, NDAttrValue, NDAttribute};
4use ad_core_rs::codec::{Codec, CodecName};
5use ad_core_rs::error::{ADError, ADResult};
6use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
7use ad_core_rs::ndarray_pool::NDArrayPool;
8use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
9use ad_core_rs::plugin::file_controller::FilePluginController;
10use ad_core_rs::plugin::runtime::{
11 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
12};
13
14use rust_hdf5::H5File;
15use rust_hdf5::format::messages::datatype::{ByteOrder, DatatypeMessage};
16use rust_hdf5::format::messages::filter::{
17 FILTER_BLOSC, FILTER_BSHUF, FILTER_JPEG, FILTER_SZIP, Filter, FilterPipeline,
18};
19use rust_hdf5::swmr::SwmrFileWriter;
20
21use crate::hdf5_layout::Hdf5Layout;
22
23const COMPRESS_NONE: i32 = 0;
25const COMPRESS_NBIT: i32 = 1;
26const COMPRESS_SZIP: i32 = 2;
27const COMPRESS_ZLIB: i32 = 3;
28const COMPRESS_BLOSC: i32 = 4;
29const COMPRESS_BSHUF: i32 = 5;
30const COMPRESS_LZ4: i32 = 6;
31const COMPRESS_JPEG: i32 = 7;
32
33const SZIP_NN_OPTION_MASK: u32 = 32;
36
37const BLOSC_LZ: i32 = 0;
39const BLOSC_LZ4: i32 = 1;
40const BLOSC_LZ4HC: i32 = 2;
41const BLOSC_SNAPPY: i32 = 3;
42const BLOSC_ZLIB: i32 = 4;
43const BLOSC_ZSTD: i32 = 5;
44
45const MAX_EXTRA_DIMS: usize = 10;
47
48const DTYPE_ATTR: &str = "NDArrayDataType";
51
52const DEFAULT_LAYOUT_XML: &str = r#"
59<hdf5_layout>
60<group name="entry">
61 <attribute name="NX_class" source="constant" value="NXentry" type="string"></attribute>
62 <group name="instrument">
63 <attribute name="NX_class" source="constant" value="NXinstrument" type="string"></attribute>
64 <group name="detector">
65 <attribute name="NX_class" source="constant" value="NXdetector" type="string"></attribute>
66 <dataset name="data" source="detector" det_default="true">
67 <attribute name="signal" source="constant" value="1" type="int"></attribute>
68 </dataset>
69 <group name="NDAttributes">
70 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
71 <dataset name="ColorMode" source="ndattribute" ndattribute="ColorMode"></dataset>
72 </group>
73 </group>
74 <group name="NDAttributes" ndattr_default="true">
75 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
76 </group>
77 <group name="performance">
78 <dataset name="timestamp"></dataset>
79 </group>
80 </group>
81 <group name="data">
82 <attribute name="NX_class" source="constant" value="NXdata" type="string"></attribute>
83 <hardlink name="data" target="/entry/instrument/detector/data"></hardlink>
84 </group>
85</group>
86</hdf5_layout>
87"#;
88
89#[derive(Clone)]
91struct ChunkConfig {
92 auto: bool,
95 n_row_chunks: usize,
96 n_col_chunks: usize,
97 n_frames_chunks: usize,
98 ndattr_chunk: usize,
101}
102
103impl Default for ChunkConfig {
104 fn default() -> Self {
105 Self {
106 auto: true,
107 n_row_chunks: 0,
108 n_col_chunks: 0,
109 n_frames_chunks: 1,
110 ndattr_chunk: 0,
111 }
112 }
113}
114
115#[derive(Clone, Default)]
117struct ExtraDim {
118 size: usize,
119 name: String,
120}
121
122struct AttributeDataset {
126 name: String,
127 description: String,
132 source: String,
133 source_type: String,
134 data_type: NDAttrDataType,
135 buffer: Vec<u8>,
137 frames: usize,
138}
139
140fn nd_attr_source_type_string(source: &NDAttrSource) -> &'static str {
143 match source {
144 NDAttrSource::Driver => "NDAttrSourceDriver",
145 NDAttrSource::Param { .. } => "NDAttrSourceParam",
146 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
147 NDAttrSource::Function(_) => "NDAttrSourceFunct",
148 NDAttrSource::Constant(_) => "NDAttrSourceConst",
149 NDAttrSource::Undefined => "Undefined",
150 }
151}
152
153impl AttributeDataset {
154 fn new(attr: &NDAttribute) -> Self {
155 Self {
156 name: attr.name.clone(),
157 description: attr.description.clone(),
158 source: attr.source.source_string().to_string(),
159 source_type: nd_attr_source_type_string(&attr.source).to_string(),
160 data_type: attr.value.data_type(),
161 buffer: Vec::new(),
162 frames: 0,
163 }
164 }
165
166 fn element_size(&self) -> usize {
169 match self.data_type {
170 NDAttrDataType::Int8 | NDAttrDataType::UInt8 => 1,
171 NDAttrDataType::Int16 | NDAttrDataType::UInt16 => 2,
172 NDAttrDataType::Int32 | NDAttrDataType::UInt32 | NDAttrDataType::Float32 => 4,
173 NDAttrDataType::Int64 | NDAttrDataType::UInt64 | NDAttrDataType::Float64 => 8,
174 NDAttrDataType::String => MAX_ATTRIBUTE_STRING_SIZE,
175 }
176 }
177
178 fn push(&mut self, value: &NDAttrValue) {
180 let es = self.element_size();
181 let mut bytes = vec![0u8; es];
182 match self.data_type {
183 NDAttrDataType::Int8 => bytes[0] = value.as_i64().unwrap_or(0) as i8 as u8,
184 NDAttrDataType::UInt8 => bytes[0] = value.as_i64().unwrap_or(0) as u8,
185 NDAttrDataType::Int16 => {
186 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i16).to_le_bytes())
187 }
188 NDAttrDataType::UInt16 => {
189 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u16).to_le_bytes())
190 }
191 NDAttrDataType::Int32 => {
192 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i32).to_le_bytes())
193 }
194 NDAttrDataType::UInt32 => {
195 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u32).to_le_bytes())
196 }
197 NDAttrDataType::Int64 => {
198 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0)).to_le_bytes())
199 }
200 NDAttrDataType::UInt64 => {
201 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u64).to_le_bytes())
202 }
203 NDAttrDataType::Float32 => {
204 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0) as f32).to_le_bytes())
205 }
206 NDAttrDataType::Float64 => {
207 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0)).to_le_bytes())
208 }
209 NDAttrDataType::String => {
210 let s = value.as_string();
211 let src = s.as_bytes();
212 let n = src.len().min(es - 1);
213 bytes[..n].copy_from_slice(&src[..n]);
214 }
215 }
216 self.buffer.extend_from_slice(&bytes);
217 self.frames += 1;
218 }
219}
220
221const MAX_ATTRIBUTE_STRING_SIZE: usize = 256;
224
225#[derive(Clone, Copy)]
236#[repr(transparent)]
237struct FixedStr256([u8; MAX_ATTRIBUTE_STRING_SIZE]);
238
239impl rust_hdf5::types::H5Type for FixedStr256 {
240 fn hdf5_type() -> rust_hdf5::format::messages::datatype::DatatypeMessage {
241 rust_hdf5::format::messages::datatype::DatatypeMessage::fixed_string(
242 MAX_ATTRIBUTE_STRING_SIZE as u32,
243 )
244 }
245
246 fn element_size() -> usize {
247 MAX_ATTRIBUTE_STRING_SIZE
248 }
249}
250
251struct DetectorDataset {
258 ds: rust_hdf5::H5Dataset,
261 frame_count: usize,
263 frame_band: Vec<Vec<u8>>,
266}
267
268enum Hdf5Handle {
270 Standard {
271 file: H5File,
272 detectors: std::collections::HashMap<String, DetectorDataset>,
276 },
277 Swmr {
278 writer: Box<SwmrFileWriter>,
280 ds_index: usize,
281 compression_dropped: bool,
284 grid: Option<SwmrGridLayout>,
289 },
290}
291
292struct SwmrGridLayout {
299 leading: Vec<usize>,
301 frame_dims: Vec<usize>,
303 chunk: Vec<usize>,
305 elem_size: usize,
307}
308
309pub struct Hdf5Writer {
311 current_path: Option<PathBuf>,
312 handle: Option<Hdf5Handle>,
313 frame_count: usize,
318 dataset_name: String,
319 open_data_type: Option<NDDataType>,
321 open_frame_dims: Option<Vec<usize>>,
323 open_codec: Option<Codec>,
328 compression_type: i32,
330 z_compress_level: u32,
331 szip_num_pixels: u32,
332 nbit_precision: u32,
333 nbit_offset: u32,
334 jpeg_quality: u32,
335 blosc_shuffle_type: i32,
336 blosc_compressor: i32,
337 blosc_compress_level: u32,
338 chunk: ChunkConfig,
340 open_mode: NDFileMode,
345 num_capture: usize,
346 n_extra_dims: usize,
347 extra_dims: [ExtraDim; MAX_EXTRA_DIMS],
348 fill_value: f64,
349 dim_att_datasets: bool,
350 swmr_mode: bool,
352 flush_nth_frame: usize,
353 pub swmr_cb_counter: u32,
354 pub store_attributes: bool,
356 pub store_performance: bool,
357 fsync_on_close: bool,
364 pub total_runtime: f64,
365 pub total_bytes: u64,
366 perf_rows: Vec<[f64; 5]>,
369 perf_prev: Option<std::time::Instant>,
370 perf_first: Option<std::time::Instant>,
371 attr_datasets: Vec<AttributeDataset>,
373 layout_filename: Option<PathBuf>,
375 layout: Option<Hdf5Layout>,
376 pub layout_valid: bool,
377 pub layout_error: String,
378 resolved_dataset_path: String,
383 resolved_ndattr_group: String,
386 resolved_perf_group: String,
388 ndattr_first_values: std::collections::HashMap<String, NDAttrValue>,
394 ndattr_last_values: std::collections::HashMap<String, NDAttrValue>,
395 ndattr_element_names: Vec<String>,
398}
399
400impl Hdf5Writer {
401 pub fn new() -> Self {
402 Self {
403 current_path: None,
404 handle: None,
405 frame_count: 0,
406 dataset_name: "data".to_string(),
407 open_data_type: None,
408 open_frame_dims: None,
409 open_codec: None,
410 compression_type: 0,
411 z_compress_level: 6,
412 szip_num_pixels: 16,
413 nbit_precision: 8,
416 nbit_offset: 0,
417 jpeg_quality: 90,
418 blosc_shuffle_type: 1,
420 blosc_compressor: 0,
421 blosc_compress_level: 5,
422 chunk: ChunkConfig::default(),
423 open_mode: NDFileMode::Single,
424 num_capture: 1,
425 n_extra_dims: 0,
426 extra_dims: Default::default(),
427 fill_value: 0.0,
428 dim_att_datasets: false,
429 swmr_mode: false,
430 flush_nth_frame: 0,
431 swmr_cb_counter: 0,
432 store_attributes: true,
433 store_performance: false,
434 fsync_on_close: Self::env_fsync_on_close(),
435 total_runtime: 0.0,
436 total_bytes: 0,
437 perf_rows: Vec::new(),
438 perf_prev: None,
439 perf_first: None,
440 attr_datasets: Vec::new(),
441 layout_filename: None,
442 layout: Some(Self::default_layout()),
444 layout_valid: false,
445 layout_error: String::new(),
446 resolved_dataset_path: "data".to_string(),
447 resolved_ndattr_group: String::new(),
448 resolved_perf_group: String::new(),
449 ndattr_first_values: std::collections::HashMap::new(),
450 ndattr_last_values: std::collections::HashMap::new(),
451 ndattr_element_names: Vec::new(),
452 }
453 }
454
455 fn env_fsync_on_close() -> bool {
457 Self::parse_fsync_on_close_env(std::env::var("AD_HDF5_FSYNC_ON_CLOSE").ok().as_deref())
458 }
459
460 fn parse_fsync_on_close_env(v: Option<&str>) -> bool {
465 match v {
466 Some(s) => !matches!(
467 s.trim().to_ascii_lowercase().as_str(),
468 "0" | "false" | "no" | "off"
469 ),
470 None => true,
471 }
472 }
473
474 pub fn set_dataset_name(&mut self, name: &str) {
475 self.dataset_name = name.to_string();
476 }
477
478 pub fn set_compression_type(&mut self, v: i32) {
479 self.compression_type = v;
480 }
481
482 pub fn set_z_compress_level(&mut self, v: u32) {
483 self.z_compress_level = v;
484 }
485
486 pub fn set_szip_num_pixels(&mut self, v: u32) {
487 self.szip_num_pixels = v;
488 }
489
490 pub fn set_blosc_shuffle_type(&mut self, v: i32) {
491 self.blosc_shuffle_type = v;
492 }
493
494 pub fn set_blosc_compressor(&mut self, v: i32) {
495 self.blosc_compressor = v;
496 }
497
498 pub fn set_blosc_compress_level(&mut self, v: u32) {
499 self.blosc_compress_level = v;
500 }
501
502 pub fn set_nbit_precision(&mut self, v: u32) {
503 self.nbit_precision = v;
504 }
505
506 pub fn set_nbit_offset(&mut self, v: u32) {
507 self.nbit_offset = v;
508 }
509
510 pub fn set_jpeg_quality(&mut self, v: u32) {
511 self.jpeg_quality = v;
512 }
513
514 pub fn set_store_attributes(&mut self, v: bool) {
515 self.store_attributes = v;
516 }
517
518 pub fn set_store_performance(&mut self, v: bool) {
519 self.store_performance = v;
520 }
521
522 pub fn set_swmr_mode(&mut self, v: bool) {
523 self.swmr_mode = v;
524 }
525
526 pub fn set_flush_nth_frame(&mut self, v: usize) {
527 self.flush_nth_frame = v;
528 }
529
530 pub fn set_chunk_size_auto(&mut self, v: bool) {
531 self.chunk.auto = v;
532 }
533
534 pub fn set_n_row_chunks(&mut self, v: usize) {
535 self.chunk.n_row_chunks = v;
536 }
537
538 pub fn set_n_col_chunks(&mut self, v: usize) {
539 self.chunk.n_col_chunks = v;
540 }
541
542 pub fn set_n_frames_chunks(&mut self, v: usize) {
543 self.chunk.n_frames_chunks = v;
544 }
545
546 pub fn set_ndattr_chunk(&mut self, v: usize) {
547 self.chunk.ndattr_chunk = v;
550 }
551
552 pub fn set_n_extra_dims(&mut self, v: usize) {
553 self.n_extra_dims = v.min(MAX_EXTRA_DIMS - 1);
559 }
560
561 pub fn set_extra_dim_size(&mut self, idx: usize, size: usize) {
562 if idx < MAX_EXTRA_DIMS {
563 self.extra_dims[idx].size = size;
564 }
565 }
566
567 pub fn set_extra_dim_name(&mut self, idx: usize, name: &str) {
568 if idx < MAX_EXTRA_DIMS {
569 self.extra_dims[idx].name = name.to_string();
570 }
571 }
572
573 pub fn set_fill_value(&mut self, v: f64) {
574 self.fill_value = v;
575 }
576
577 pub fn set_dim_att_datasets(&mut self, v: bool) {
578 self.dim_att_datasets = v;
579 }
580
581 fn default_layout() -> Hdf5Layout {
585 Hdf5Layout::parse(DEFAULT_LAYOUT_XML).expect("built-in default layout must parse")
586 }
587
588 pub fn set_layout_filename(&mut self, path: &str) -> bool {
591 if path.trim().is_empty() {
592 self.layout_filename = None;
596 self.layout = Some(Self::default_layout());
597 self.layout_valid = false;
598 self.layout_error.clear();
599 return true;
600 }
601 let p = PathBuf::from(path);
602 match Hdf5Layout::from_file(&p) {
603 Ok(layout) => {
604 self.layout_filename = Some(p);
605 self.layout = Some(layout);
606 self.layout_valid = true;
607 self.layout_error.clear();
608 true
609 }
610 Err(e) => {
611 self.layout_filename = Some(p);
612 self.layout = None;
613 self.layout_valid = false;
614 self.layout_error = e.0;
615 false
616 }
617 }
618 }
619
620 pub fn frame_count(&self) -> usize {
621 self.frame_count
622 }
623
624 pub fn flush_swmr(&mut self) {
626 if let Some(Hdf5Handle::Swmr { ref mut writer, .. }) = self.handle {
627 if writer.flush().is_ok() {
628 self.swmr_cb_counter += 1;
629 }
630 }
631 }
632
633 pub fn is_swmr_active(&self) -> bool {
635 matches!(self.handle, Some(Hdf5Handle::Swmr { .. }))
636 }
637
638 pub fn swmr_compression_dropped(&self) -> bool {
640 matches!(
641 self.handle,
642 Some(Hdf5Handle::Swmr {
643 compression_dropped: true,
644 ..
645 })
646 )
647 }
648
649 fn build_pipeline(&self, element_size: usize) -> Option<FilterPipeline> {
651 match self.compression_type {
652 COMPRESS_NONE => None,
653 COMPRESS_ZLIB => Some(FilterPipeline::deflate(self.z_compress_level)),
654 COMPRESS_SZIP => Some(FilterPipeline {
655 filters: vec![Filter {
656 id: FILTER_SZIP,
657 flags: 0,
658 cd_values: vec![SZIP_NN_OPTION_MASK, self.szip_num_pixels],
663 }],
664 }),
665 COMPRESS_LZ4 => Some(FilterPipeline::lz4()),
666 COMPRESS_BSHUF => Some(FilterPipeline {
667 filters: vec![Filter {
671 id: FILTER_BSHUF,
672 flags: 0,
673 cd_values: vec![0, 0, element_size as u32, 0, 2],
674 }],
675 }),
676 COMPRESS_BLOSC => {
677 let compressor_code = match self.blosc_compressor {
678 BLOSC_LZ => 0,
679 BLOSC_LZ4 => 1,
680 BLOSC_LZ4HC => 2,
681 BLOSC_SNAPPY => 3,
682 BLOSC_ZLIB => 4,
683 BLOSC_ZSTD => 5,
684 _ => 0,
685 };
686 Some(FilterPipeline {
687 filters: vec![Filter {
688 id: FILTER_BLOSC,
689 flags: 0,
690 cd_values: vec![
691 2,
692 2,
693 element_size as u32,
694 0,
695 self.blosc_compress_level,
696 self.blosc_shuffle_type as u32,
697 compressor_code,
698 ],
699 }],
700 })
701 }
702 COMPRESS_NBIT => {
703 None
719 }
720 COMPRESS_JPEG => Some(FilterPipeline {
721 filters: vec![Filter {
722 id: FILTER_JPEG,
723 flags: 0,
724 cd_values: vec![self.jpeg_quality],
725 }],
726 }),
727 _ => None,
728 }
729 }
730
731 fn nbit_packing(
755 &self,
756 data_type: NDDataType,
757 chunk_nelmts: usize,
758 ) -> Option<(DatatypeMessage, FilterPipeline)> {
759 if self.compression_type != COMPRESS_NBIT {
760 return None;
761 }
762 let (size, signed) = match data_type {
763 NDDataType::Int8 => (1u32, true),
764 NDDataType::UInt8 => (1, false),
765 NDDataType::Int16 => (2, true),
766 NDDataType::UInt16 => (2, false),
767 NDDataType::Int32 => (4, true),
768 NDDataType::UInt32 => (4, false),
769 NDDataType::Int64 => (8, true),
770 NDDataType::UInt64 => (8, false),
771 NDDataType::Float32 | NDDataType::Float64 => return None,
772 };
773 if self.nbit_precision == 0 || self.nbit_precision > size * 8 {
774 return None;
775 }
776 let dt = DatatypeMessage::FixedPoint {
777 size,
778 byte_order: ByteOrder::LittleEndian,
779 signed,
780 bit_offset: self.nbit_offset as u16,
781 bit_precision: self.nbit_precision as u16,
782 };
783 let pipeline = FilterPipeline::nbit(&dt, chunk_nelmts);
784 Some((dt, pipeline))
785 }
786
787 fn codec_filter_pipeline(&self, codec: &Codec) -> Option<FilterPipeline> {
800 let element_size = codec.original_data_type.element_size() as u32;
801 match codec.name {
802 CodecName::LZ4 => Some(FilterPipeline::lz4()),
803 CodecName::BSLZ4 => Some(FilterPipeline {
804 filters: vec![Filter {
808 id: FILTER_BSHUF,
809 flags: 0,
810 cd_values: vec![0, 0, element_size, 0, 2],
811 }],
812 }),
813 CodecName::Blosc => {
814 Some(FilterPipeline {
818 filters: vec![Filter {
819 id: FILTER_BLOSC,
820 flags: 0,
821 cd_values: vec![
822 2,
823 2,
824 element_size,
825 0,
826 codec.level.max(0) as u32,
827 codec.shuffle.max(0) as u32,
828 codec.compressor.max(0) as u32,
829 ],
830 }],
831 })
832 }
833 CodecName::JPEG => Some(FilterPipeline {
839 filters: vec![Filter {
840 id: FILTER_JPEG,
841 flags: 0,
842 cd_values: vec![self.jpeg_quality],
843 }],
844 }),
845 CodecName::None | CodecName::Zlib | CodecName::LZ4HDF5 => None,
846 }
847 }
848
849 fn frame_chunk_dims(&self, frame_dims: &[usize]) -> Vec<usize> {
856 if frame_dims.len() == 2 {
857 let y = frame_dims[0].max(1);
858 let x = frame_dims[1].max(1);
859 vec![
860 Self::clamp_chunk(self.chunk.n_row_chunks, y, self.chunk.auto),
861 Self::clamp_chunk(self.chunk.n_col_chunks, x, self.chunk.auto),
862 ]
863 } else {
864 frame_dims.iter().map(|&d| d.max(1)).collect()
865 }
866 }
867
868 fn extra_dim_axes(&self) -> Option<Vec<usize>> {
878 if self.n_extra_dims == 0 {
879 return None;
880 }
881 Some(
882 (0..=self.n_extra_dims)
883 .rev()
884 .map(|i| self.extra_dims[i].size.max(1))
885 .collect(),
886 )
887 }
888
889 fn standard_layout(
905 &self,
906 frame_dims: &[usize],
907 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
908 let frame_chunk = self.frame_chunk_dims(frame_dims);
909 match self.extra_dim_axes() {
910 Some(leading) => {
911 let mut shape = leading.clone();
912 shape.extend_from_slice(frame_dims);
913 let mut chunk = vec![1usize; leading.len()];
916 chunk.extend_from_slice(&frame_chunk);
917 (shape, chunk, Some(leading))
918 }
919 None => {
920 let mut shape = vec![1usize];
921 shape.extend_from_slice(frame_dims);
922 let mut chunk = vec![self.chunk.n_frames_chunks.max(1)];
923 chunk.extend_from_slice(&frame_chunk);
924 (shape, chunk, None)
925 }
926 }
927 }
928
929 fn compressed_layout(
939 &self,
940 frame_dims: &[usize],
941 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
942 let frame_chunk: Vec<usize> = frame_dims.iter().map(|&d| d.max(1)).collect();
943 match self.extra_dim_axes() {
944 Some(leading) => {
945 let mut shape = leading.clone();
946 shape.extend_from_slice(frame_dims);
947 let mut chunk = vec![1usize; leading.len()];
948 chunk.extend_from_slice(&frame_chunk);
949 (shape, chunk, Some(leading))
950 }
951 None => {
952 let mut shape = vec![1usize];
953 shape.extend_from_slice(frame_dims);
954 let mut chunk = vec![1usize];
955 chunk.extend_from_slice(&frame_chunk);
956 (shape, chunk, None)
957 }
958 }
959 }
960
961 fn primary_layout(&self, frame_dims: &[usize]) -> (Vec<usize>, Vec<usize>, Option<usize>) {
972 let extra_extent = if self.n_extra_dims > 0 {
973 Some(
974 (0..self.n_extra_dims)
975 .map(|i| self.extra_dims[i].size.max(1))
976 .product::<usize>(),
977 )
978 } else {
979 None
980 };
981
982 let mut shape: Vec<usize> = vec![extra_extent.unwrap_or(1)];
983 shape.extend_from_slice(frame_dims);
984
985 let mut chunk = vec![if extra_extent.is_some() {
986 1
987 } else {
988 self.chunk.n_frames_chunks.max(1)
989 }];
990 chunk.extend_from_slice(&self.frame_chunk_dims(frame_dims));
991 (shape, chunk, extra_extent)
992 }
993
994 fn unravel(mut idx: usize, dims: &[usize]) -> Vec<usize> {
1000 let mut coords = vec![0usize; dims.len()];
1001 for d in (0..dims.len()).rev() {
1002 let s = dims[d].max(1);
1003 coords[d] = idx % s;
1004 idx /= s;
1005 }
1006 coords
1007 }
1008
1009 fn clamp_chunk(requested: usize, dim: usize, auto: bool) -> usize {
1012 if auto || requested == 0 || requested > dim {
1013 dim
1014 } else {
1015 requested
1016 }
1017 }
1018
1019 fn flush_band(
1035 ds: &rust_hdf5::H5Dataset,
1036 leading_coords: &[usize],
1037 frames: &[Vec<u8>],
1038 frame_dims: &[usize],
1039 chunk: &[usize],
1040 elem_size: usize,
1041 ) -> ADResult<()> {
1042 for (coords, buf) in
1043 Self::band_chunk_writes(leading_coords, frames, frame_dims, chunk, elem_size)
1044 {
1045 ds.write_chunk_at(&coords, &buf).map_err(|e| {
1046 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
1047 })?;
1048 }
1049 Ok(())
1050 }
1051
1052 fn band_chunk_writes(
1065 leading_coords: &[usize],
1066 frames: &[Vec<u8>],
1067 frame_dims: &[usize],
1068 chunk: &[usize],
1069 elem_size: usize,
1070 ) -> Vec<(Vec<usize>, Vec<u8>)> {
1071 let lead = leading_coords.len();
1072 let fc = chunk[0];
1073 if frame_dims.len() != 2 {
1076 let frame_len = frame_dims.iter().product::<usize>() * elem_size;
1077 let mut buf = vec![0u8; fc * frame_len];
1078 for (f, fb) in frames.iter().take(fc).enumerate() {
1079 buf[f * frame_len..f * frame_len + frame_len].copy_from_slice(fb);
1080 }
1081 let mut coords = leading_coords.to_vec();
1082 coords.extend(std::iter::repeat_n(0, frame_dims.len()));
1083 return vec![(coords, buf)];
1084 }
1085
1086 let (y, x) = (frame_dims[0], frame_dims[1]);
1087 let (rc, cc) = (chunk[lead], chunk[lead + 1]);
1088 let row_tiles = y.div_ceil(rc);
1089 let col_tiles = x.div_ceil(cc);
1090 let mut writes = Vec::with_capacity(row_tiles * col_tiles);
1091 for ry in 0..row_tiles {
1092 for cx in 0..col_tiles {
1093 let mut tile = vec![0u8; fc * rc * cc * elem_size];
1094 for f in 0..fc {
1095 let Some(fb) = frames.get(f) else {
1096 break; };
1098 for r in 0..rc {
1099 let sy = ry * rc + r;
1100 if sy >= y {
1101 break;
1102 }
1103 for c in 0..cc {
1104 let sx = cx * cc + c;
1105 if sx >= x {
1106 break;
1107 }
1108 let src = (sy * x + sx) * elem_size;
1109 let dst = ((f * rc + r) * cc + c) * elem_size;
1110 tile[dst..dst + elem_size].copy_from_slice(&fb[src..src + elem_size]);
1111 }
1112 }
1113 }
1114 let mut coords = leading_coords.to_vec();
1115 coords.push(ry);
1116 coords.push(cx);
1117 writes.push((coords, tile));
1118 }
1119 }
1120 writes
1121 }
1122
1123 fn finalize_standard_datasets(&mut self) -> ADResult<()> {
1129 let Some(frame_dims) = self.open_frame_dims.clone() else {
1130 return Ok(());
1131 };
1132 let (_, chunk, leading) = self.standard_layout(&frame_dims);
1133 let elem_size = self.open_data_type.map(|t| t.element_size()).unwrap_or(1);
1134 let fc = chunk[0];
1135 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
1136 return Ok(());
1137 };
1138 for det in detectors.values_mut() {
1139 let total = det.frame_count;
1140 if !det.frame_band.is_empty() {
1143 let last = total.saturating_sub(1);
1144 let leading_coords = match &leading {
1145 Some(lead) => Self::unravel(last, lead),
1146 None => vec![last / fc],
1147 };
1148 Self::flush_band(
1149 &det.ds,
1150 &leading_coords,
1151 &det.frame_band,
1152 &frame_dims,
1153 &chunk,
1154 elem_size,
1155 )?;
1156 det.frame_band.clear();
1157 }
1158 if total > 0 {
1163 let mut dims = leading.clone().unwrap_or_else(|| vec![total]);
1164 dims.extend_from_slice(&frame_dims);
1165 det.ds.set_extent(&dims).map_err(|e| {
1166 ADError::UnsupportedConversion(format!("HDF5 set_extent error: {}", e))
1167 })?;
1168 }
1169 }
1170 Ok(())
1171 }
1172
1173 fn open_swmr(&mut self, path: &Path, array: &NDArray) -> ADResult<()> {
1186 let mut swmr = SwmrFileWriter::create(path)
1187 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR create error: {}", e)))?;
1188
1189 let usize_frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1190 let frame_dims: Vec<u64> = usize_frame_dims.iter().map(|&d| d as u64).collect();
1191
1192 let element_size = array.data.data_type().element_size();
1198 let pipeline = self.build_pipeline(element_size);
1199 let chunk: Vec<u64> = {
1200 let (_, c, _) = self.primary_layout(&usize_frame_dims);
1201 c.iter().map(|&v| v as u64).collect()
1202 };
1203
1204 let (grid_shape_usize, grid_chunk_usize, grid_leading) =
1210 self.standard_layout(&usize_frame_dims);
1211 let use_grid = grid_leading.is_some() && pipeline.is_none();
1212 let grid_shape: Vec<u64> = grid_shape_usize.iter().map(|&v| v as u64).collect();
1213 let grid_chunk: Vec<u64> = grid_chunk_usize.iter().map(|&v| v as u64).collect();
1214
1215 let ds_group_path: Option<String> = self
1223 .resolved_dataset_path
1224 .rsplit_once('/')
1225 .map(|(group_path, _leaf)| group_path.to_string());
1226 let ds_name = self.resolved_dataset_path.clone();
1227
1228 macro_rules! create_ds {
1229 ($t:ty) => {
1230 if use_grid {
1231 swmr.create_grid_dataset::<$t>(&ds_name, &grid_shape, &grid_chunk)
1232 .map_err(|e| {
1233 ADError::UnsupportedConversion(format!(
1234 "SWMR create grid dataset error: {}",
1235 e
1236 ))
1237 })
1238 } else {
1239 match pipeline.clone() {
1240 Some(pl) => swmr
1241 .create_streaming_dataset_chunked_compressed::<$t>(
1242 &ds_name,
1243 &frame_dims,
1244 &chunk,
1245 pl,
1246 )
1247 .map_err(|e| {
1248 ADError::UnsupportedConversion(format!(
1249 "SWMR create compressed dataset error: {}",
1250 e
1251 ))
1252 }),
1253 None => swmr
1254 .create_streaming_dataset_chunked::<$t>(&ds_name, &frame_dims, &chunk)
1255 .map_err(|e| {
1256 ADError::UnsupportedConversion(format!(
1257 "SWMR create dataset error: {}",
1258 e
1259 ))
1260 }),
1261 }
1262 }
1263 };
1264 }
1265
1266 let ds_index = match array.data.data_type() {
1267 NDDataType::Int8 => create_ds!(i8)?,
1268 NDDataType::UInt8 => create_ds!(u8)?,
1269 NDDataType::Int16 => create_ds!(i16)?,
1270 NDDataType::UInt16 => create_ds!(u16)?,
1271 NDDataType::Int32 => create_ds!(i32)?,
1272 NDDataType::UInt32 => create_ds!(u32)?,
1273 NDDataType::Int64 => create_ds!(i64)?,
1274 NDDataType::UInt64 => create_ds!(u64)?,
1275 NDDataType::Float32 => create_ds!(f32)?,
1276 NDDataType::Float64 => create_ds!(f64)?,
1277 };
1278
1279 self.build_swmr_layout_groups(&mut swmr)?;
1286 if let Some(ref group_path) = ds_group_path {
1287 let abs_group = format!("/{}", group_path);
1290 swmr.assign_dataset_to_group(&abs_group, ds_index)
1291 .map_err(|e| {
1292 ADError::UnsupportedConversion(format!(
1293 "SWMR assign dataset to group '{}': {}",
1294 abs_group, e
1295 ))
1296 })?;
1297 }
1298 self.write_swmr_layout_dataset_attrs(&mut swmr, ds_index)?;
1299 self.write_swmr_ndarray_default_attrs(&mut swmr, ds_index, array)?;
1300 self.build_swmr_layout_hardlinks(&mut swmr)?;
1301 self.write_swmr_ndattr_element_attrs(&mut swmr, ds_index)?;
1305
1306 swmr.start_swmr()
1307 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR start error: {}", e)))?;
1308
1309 let compression_dropped = self.compression_type != COMPRESS_NONE && pipeline.is_none();
1314 if compression_dropped {
1315 eprintln!(
1316 "NDFileHDF5: WARNING — SWMR mode requested compression type {} \
1317 but no filter pipeline could be built for it; the SWMR file \
1318 will be written UNCOMPRESSED.",
1319 self.compression_type
1320 );
1321 }
1322
1323 let grid = if use_grid {
1324 grid_leading.map(|leading| SwmrGridLayout {
1326 leading,
1327 frame_dims: usize_frame_dims.clone(),
1328 chunk: grid_chunk_usize.clone(),
1329 elem_size: element_size,
1330 })
1331 } else {
1332 None
1333 };
1334 self.handle = Some(Hdf5Handle::Swmr {
1335 writer: Box::new(swmr),
1336 ds_index,
1337 compression_dropped,
1338 grid,
1339 });
1340 self.open_data_type = Some(array.data.data_type());
1341 self.open_frame_dims = Some(array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>());
1342 Ok(())
1343 }
1344
1345 fn build_swmr_layout_groups(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1354 let layout = match self.layout.as_ref() {
1355 Some(l) => l,
1356 None => return Ok(()),
1357 };
1358 fn collect<'a>(
1359 g: &'a crate::hdf5_layout::LayoutGroup,
1360 prefix: &str,
1361 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1362 ) {
1363 let here = if prefix.is_empty() {
1364 g.name.clone()
1365 } else {
1366 format!("{}/{}", prefix, g.name)
1367 };
1368 out.push((here.clone(), g));
1369 for sub in &g.groups {
1370 collect(sub, &here, out);
1371 }
1372 }
1373 let mut nodes = Vec::new();
1374 for g in &layout.groups {
1375 collect(g, "", &mut nodes);
1376 }
1377 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1378 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1379 for (path, node) in &nodes {
1380 if !created.insert(path.clone()) {
1381 continue;
1382 }
1383 let (parent, leaf) = match path.rsplit_once('/') {
1384 Some((p, l)) => (format!("/{}", p), l),
1385 None => ("/".to_string(), path.as_str()),
1386 };
1387 swmr.create_group(&parent, leaf).map_err(|e| {
1388 ADError::UnsupportedConversion(format!("SWMR layout group '{}': {}", path, e))
1389 })?;
1390 write_swmr_group_constant_attrs(swmr, &format!("/{}", path), &node.attributes)?;
1394 }
1395 Ok(())
1396 }
1397
1398 fn build_swmr_layout_hardlinks(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1408 let layout = match self.layout.as_ref() {
1409 Some(l) => l,
1410 None => return Ok(()),
1411 };
1412 fn collect<'a>(
1413 g: &'a crate::hdf5_layout::LayoutGroup,
1414 prefix: &str,
1415 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1416 ) {
1417 let here = if prefix.is_empty() {
1418 g.name.clone()
1419 } else {
1420 format!("{}/{}", prefix, g.name)
1421 };
1422 for hl in &g.hardlinks {
1423 out.push((here.clone(), hl));
1424 }
1425 for sub in &g.groups {
1426 collect(sub, &here, out);
1427 }
1428 }
1429 let mut links = Vec::new();
1430 for g in &layout.groups {
1431 collect(g, "", &mut links);
1432 }
1433 for (parent_path, hl) in &links {
1434 let parent = format!("/{}", parent_path);
1435 swmr.create_hard_link(&parent, &hl.name, &hl.target)
1436 .map_err(|e| {
1437 ADError::UnsupportedConversion(format!(
1438 "SWMR layout hardlink '{}/{}' -> '{}': {}",
1439 parent_path, hl.name, hl.target, e
1440 ))
1441 })?;
1442 }
1443 Ok(())
1444 }
1445
1446 fn write_swmr_layout_dataset_attrs(
1454 &self,
1455 swmr: &mut SwmrFileWriter,
1456 ds_index: usize,
1457 ) -> ADResult<()> {
1458 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
1459 let layout = match self.layout.as_ref() {
1460 Some(l) => l,
1461 None => return Ok(()),
1462 };
1463 let resolved_ds = self.resolved_dataset_path.as_str();
1464 let mut attrs: Vec<(String, LayoutDataType, String)> = Vec::new();
1465 layout.for_each_dataset(|path, d| {
1466 let full = format!("{}/{}", path, d.name);
1467 if full.trim_start_matches('/') == resolved_ds {
1468 for a in &d.attributes {
1469 if a.source == LayoutSource::Constant {
1470 attrs.push((a.name.clone(), a.data_type, a.value.clone()));
1471 }
1472 }
1473 }
1474 });
1475 for (name, dtype, value) in &attrs {
1476 match dtype {
1477 LayoutDataType::Int => {
1478 let v: i64 = value.trim().parse().unwrap_or(0);
1479 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1480 }
1481 LayoutDataType::Float => {
1482 let v: f64 = value.trim().parse().unwrap_or(0.0);
1483 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1484 }
1485 LayoutDataType::String => swmr.set_dataset_attr_string(ds_index, name, value),
1486 }
1487 .map_err(|e| {
1488 ADError::UnsupportedConversion(format!(
1489 "SWMR layout dataset attribute '{}': {}",
1490 name, e
1491 ))
1492 })?;
1493 }
1494 Ok(())
1495 }
1496
1497 fn resolve_layout_paths(&mut self) {
1509 let strip = |s: String| s.trim_start_matches('/').to_string();
1510 match self.layout.as_ref() {
1511 Some(layout) => {
1512 self.resolved_dataset_path = layout
1513 .detector_dataset_path()
1514 .map(strip)
1515 .unwrap_or_else(|| self.dataset_name.clone());
1516 self.resolved_ndattr_group =
1517 layout.ndattr_default_group().map(strip).unwrap_or_default();
1518 self.resolved_perf_group = layout
1519 .dataset_group_path("timestamp")
1520 .map(strip)
1521 .unwrap_or_default();
1522 }
1523 None => {
1524 self.resolved_dataset_path = self.dataset_name.clone();
1525 self.resolved_ndattr_group.clear();
1526 self.resolved_perf_group.clear();
1527 }
1528 }
1529 }
1530
1531 fn build_layout_groups(&self, file: &H5File) -> ADResult<()> {
1540 let layout = match self.layout.as_ref() {
1541 Some(l) => l,
1542 None => return Ok(()),
1543 };
1544 fn collect<'a>(
1545 g: &'a crate::hdf5_layout::LayoutGroup,
1546 prefix: &str,
1547 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1548 ) {
1549 let here = if prefix.is_empty() {
1550 g.name.clone()
1551 } else {
1552 format!("{}/{}", prefix, g.name)
1553 };
1554 out.push((here.clone(), g));
1555 for sub in &g.groups {
1556 collect(sub, &here, out);
1557 }
1558 }
1559 let mut nodes = Vec::new();
1560 for g in &layout.groups {
1561 collect(g, "", &mut nodes);
1562 }
1563 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1564 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1565 for (path, node) in &nodes {
1566 if !created.insert(path.clone()) {
1567 continue;
1568 }
1569 let (parent, leaf) = match path.rsplit_once('/') {
1570 Some((p, l)) => (p, l),
1571 None => ("", path.as_str()),
1572 };
1573 let parent_group = if parent.is_empty() {
1575 None
1576 } else {
1577 Some(Self::open_write_group(file, parent)?)
1578 };
1579 let group = match parent_group.as_ref() {
1580 Some(g) => g.create_group(leaf),
1581 None => file.create_group(leaf),
1582 }
1583 .map_err(|e| {
1584 ADError::UnsupportedConversion(format!("HDF5 layout group '{}': {}", path, e))
1585 })?;
1586 write_group_constant_attrs(&group, &node.attributes);
1589 }
1590 Ok(())
1591 }
1592
1593 fn build_layout_hardlinks(&self, file: &H5File) -> ADResult<()> {
1611 let layout = match self.layout.as_ref() {
1612 Some(l) => l,
1613 None => return Ok(()),
1614 };
1615 fn collect<'a>(
1617 g: &'a crate::hdf5_layout::LayoutGroup,
1618 prefix: &str,
1619 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1620 ) {
1621 let here = if prefix.is_empty() {
1622 g.name.clone()
1623 } else {
1624 format!("{}/{}", prefix, g.name)
1625 };
1626 for hl in &g.hardlinks {
1627 out.push((here.clone(), hl));
1628 }
1629 for sub in &g.groups {
1630 collect(sub, &here, out);
1631 }
1632 }
1633 let mut links = Vec::new();
1634 for g in &layout.groups {
1635 collect(g, "", &mut links);
1636 }
1637 for (parent_path, hl) in &links {
1638 let parent = Self::open_write_group(file, parent_path)?;
1641 parent.link(&hl.name, &hl.target).map_err(|e| {
1642 ADError::UnsupportedConversion(format!(
1643 "HDF5 layout hardlink '{}/{}' -> '{}': {}",
1644 parent_path, hl.name, hl.target, e
1645 ))
1646 })?;
1647 }
1648 Ok(())
1649 }
1650
1651 fn open_write_group(file: &H5File, path: &str) -> ADResult<rust_hdf5::H5Group> {
1655 let mut current: Option<rust_hdf5::H5Group> = None;
1656 for seg in path.split('/').filter(|s| !s.is_empty()) {
1657 let next = match current.as_ref() {
1658 Some(g) => g.group(seg),
1659 None => file.root_group().group(seg),
1660 }
1661 .map_err(|e| {
1662 ADError::UnsupportedConversion(format!("HDF5 group reopen '{}': {}", seg, e))
1663 })?;
1664 current = Some(next);
1665 }
1666 current.ok_or_else(|| ADError::UnsupportedConversion("empty group path".into()))
1667 }
1668
1669 fn create_detector_datasets(&mut self, array: &NDArray) -> ADResult<()> {
1677 let frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1678 let (ds_data_type, shape, chunk, leading, pipeline) = match array.codec.as_ref() {
1686 Some(codec) => {
1687 let pl = self.codec_filter_pipeline(codec).ok_or_else(|| {
1688 ADError::UnsupportedConversion(format!(
1689 "HDF5 cannot direct-chunk-write codec '{}' \
1690 (only jpeg/blosc/lz4/bslz4 are supported)",
1691 codec.name.as_str()
1692 ))
1693 })?;
1694 let (shape, chunk, leading) = self.compressed_layout(&frame_dims);
1695 (codec.original_data_type, shape, chunk, leading, Some(pl))
1696 }
1697 None => {
1698 let dt = array.data.data_type();
1699 let (shape, chunk, leading) = self.standard_layout(&frame_dims);
1700 (
1701 dt,
1702 shape,
1703 chunk,
1704 leading,
1705 self.build_pipeline(dt.element_size()),
1706 )
1707 }
1708 };
1709 let (nbit_dt, pipeline): (Option<DatatypeMessage>, Option<FilterPipeline>) =
1715 if array.codec.is_none() {
1716 let chunk_nelmts: usize = chunk.iter().product();
1717 match self.nbit_packing(ds_data_type, chunk_nelmts) {
1718 Some((dt, pl)) => (Some(dt), Some(pl)),
1719 None => (None, pipeline),
1720 }
1721 } else {
1722 (None, pipeline)
1723 };
1724 let max_shape: Vec<Option<usize>> = shape
1732 .iter()
1733 .zip(chunk.iter())
1734 .enumerate()
1735 .map(|(i, (&s, &c))| {
1736 if leading.is_none() && i == 0 {
1737 None
1738 } else {
1739 Some(s.div_ceil(c) * c)
1740 }
1741 })
1742 .collect();
1743
1744 match self.handle {
1748 Some(Hdf5Handle::Standard { ref file, .. }) => self.build_layout_groups(file)?,
1749 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1750 }
1751
1752 let detector_keys: Vec<String> = match self.layout.as_ref() {
1759 Some(l) => {
1760 let mut keys = l.detector_dataset_paths();
1761 if keys.is_empty() {
1762 keys.push(format!("/{}", self.resolved_dataset_path));
1763 }
1764 keys
1765 }
1766 None => vec![format!("/{}", self.resolved_dataset_path)],
1767 };
1768 let detector_specs: Vec<(
1769 String,
1770 Vec<(String, crate::hdf5_layout::LayoutDataType, String)>,
1771 )> = detector_keys
1772 .iter()
1773 .map(|key| {
1774 let stripped = key.trim_start_matches('/').to_string();
1775 let attrs = self
1776 .layout
1777 .as_ref()
1778 .map(|l| {
1779 use crate::hdf5_layout::LayoutSource;
1780 let mut out = Vec::new();
1781 l.for_each_dataset(|path, d| {
1782 let full = format!("{}/{}", path, d.name);
1783 if full.trim_start_matches('/') == stripped {
1784 for a in &d.attributes {
1785 if a.source == LayoutSource::Constant {
1786 out.push((a.name.clone(), a.data_type, a.value.clone()));
1787 }
1788 }
1789 }
1790 });
1791 out
1792 })
1793 .unwrap_or_default();
1794 (key.clone(), attrs)
1795 })
1796 .collect();
1797
1798 let dtype_ordinal = ds_data_type as i32;
1799 let fill = self.fill_value;
1800 let row_chunks = self.chunk.n_row_chunks as i32;
1801 let col_chunks = self.chunk.n_col_chunks as i32;
1802 let frame_chunks = self.chunk.n_frames_chunks as i32;
1803 let n_extra = self.n_extra_dims as i32;
1804 let extra_meta: Vec<(usize, i32, String)> = (0..self.n_extra_dims)
1805 .map(|i| {
1806 (
1807 i,
1808 self.extra_dims[i].size.max(1) as i32,
1809 self.extra_dims[i].name.clone(),
1810 )
1811 })
1812 .collect();
1813
1814 let nd_num_dims = array.dims.len() as i32;
1819 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
1823 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
1824 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
1825
1826 macro_rules! create_ds {
1827 ($t:ty, $h5file:expr, $ds_group:expr, $ds_name:expr, $constant_attrs:expr) => {{
1828 let mut builder = match $ds_group.as_ref() {
1829 Some(g) => g.new_dataset::<$t>(),
1830 None => $h5file.new_dataset::<$t>(),
1831 }
1832 .shape(&shape[..])
1833 .chunk(&chunk[..])
1834 .max_shape(&max_shape[..])
1835 .fill_value(fill as $t);
1841 if let Some(ref dt) = nbit_dt {
1845 builder = builder.datatype(dt.clone());
1846 }
1847 if let Some(ref pl) = pipeline {
1848 builder = builder.filter_pipeline(pl.clone());
1849 }
1850 let ds = builder.create($ds_name.as_str()).map_err(|e| {
1851 ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e))
1852 })?;
1853 let _ = ds
1855 .new_attr::<i32>()
1856 .shape(())
1857 .create(DTYPE_ATTR)
1858 .and_then(|a| a.write_numeric(&dtype_ordinal));
1859 let _ = ds
1863 .new_attr::<f64>()
1864 .shape(())
1865 .create("HDF5_fillValue")
1866 .and_then(|a| a.write_numeric(&fill));
1867 for (name, val) in [
1871 ("HDF5_nRowChunks", row_chunks),
1872 ("HDF5_nColChunks", col_chunks),
1873 ("HDF5_nFramesChunks", frame_chunks),
1874 ("HDF5_nExtraDims", n_extra),
1875 ] {
1876 let _ = ds
1877 .new_attr::<i32>()
1878 .shape(())
1879 .create(name)
1880 .and_then(|a| a.write_numeric(&val));
1881 }
1882 for (i, size, name) in &extra_meta {
1885 let _ = ds
1886 .new_attr::<i32>()
1887 .shape(())
1888 .create(&format!("HDF5_extraDimSize{}", i))
1889 .and_then(|a| a.write_numeric(size));
1890 if !name.is_empty() {
1891 let s = rust_hdf5::types::VarLenUnicode(name.clone());
1892 let _ = ds
1893 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1894 .shape(())
1895 .create(&format!("HDF5_extraDimName{}", i))
1896 .and_then(|a| a.write_scalar(&s));
1897 }
1898 }
1899 for (aname, atype, avalue) in $constant_attrs {
1902 use crate::hdf5_layout::LayoutDataType;
1903 match atype {
1904 LayoutDataType::Int => {
1905 let v: i64 = avalue.trim().parse().unwrap_or(0);
1906 let _ = ds
1907 .new_attr::<i64>()
1908 .shape(())
1909 .create(aname)
1910 .and_then(|a| a.write_numeric(&v));
1911 }
1912 LayoutDataType::Float => {
1913 let v: f64 = avalue.trim().parse().unwrap_or(0.0);
1914 let _ = ds
1915 .new_attr::<f64>()
1916 .shape(())
1917 .create(aname)
1918 .and_then(|a| a.write_numeric(&v));
1919 }
1920 LayoutDataType::String => {
1921 let s = rust_hdf5::types::VarLenUnicode(avalue.clone());
1922 let _ = ds
1923 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1924 .shape(())
1925 .create(aname)
1926 .and_then(|a| a.write_scalar(&s));
1927 }
1928 }
1929 }
1930 let _ = ds
1936 .new_attr::<i32>()
1937 .shape(())
1938 .create("NDArrayNumDims")
1939 .and_then(|a| a.write_numeric(&nd_num_dims));
1940 for (name, vals) in [
1941 ("NDArrayDimOffset", &dim_offsets),
1942 ("NDArrayDimBinning", &dim_binnings),
1943 ("NDArrayDimReverse", &dim_reverses),
1944 ] {
1945 let _ = if vals.len() == 1 {
1946 ds.new_attr::<i32>()
1947 .shape(())
1948 .create(name)
1949 .and_then(|a| a.write_numeric(&vals[0]))
1950 } else {
1951 ds.new_attr::<i32>()
1952 .shape([vals.len()])
1953 .create(name)
1954 .and_then(|a| a.write_array(vals))
1955 };
1956 }
1957 ds
1958 }};
1959 }
1960
1961 let h5file = match self.handle {
1962 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1963 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1964 };
1965
1966 let mut detectors: std::collections::HashMap<String, DetectorDataset> =
1967 std::collections::HashMap::with_capacity(detector_specs.len());
1968 for (key, constant_attrs) in &detector_specs {
1969 let stripped = key.trim_start_matches('/');
1972 let (ds_group, ds_name): (Option<rust_hdf5::H5Group>, String) =
1973 match stripped.rsplit_once('/') {
1974 Some((group_path, leaf)) => (
1975 Some(Self::open_write_group(h5file, group_path)?),
1976 leaf.to_string(),
1977 ),
1978 None => (None, stripped.to_string()),
1979 };
1980 let ds = match ds_data_type {
1985 NDDataType::Int8 => create_ds!(i8, h5file, ds_group, ds_name, constant_attrs),
1986 NDDataType::UInt8 => create_ds!(u8, h5file, ds_group, ds_name, constant_attrs),
1987 NDDataType::Int16 => create_ds!(i16, h5file, ds_group, ds_name, constant_attrs),
1988 NDDataType::UInt16 => create_ds!(u16, h5file, ds_group, ds_name, constant_attrs),
1989 NDDataType::Int32 => create_ds!(i32, h5file, ds_group, ds_name, constant_attrs),
1990 NDDataType::UInt32 => create_ds!(u32, h5file, ds_group, ds_name, constant_attrs),
1991 NDDataType::Int64 => create_ds!(i64, h5file, ds_group, ds_name, constant_attrs),
1992 NDDataType::UInt64 => create_ds!(u64, h5file, ds_group, ds_name, constant_attrs),
1993 NDDataType::Float32 => create_ds!(f32, h5file, ds_group, ds_name, constant_attrs),
1994 NDDataType::Float64 => create_ds!(f64, h5file, ds_group, ds_name, constant_attrs),
1995 };
1996 detectors.insert(
1997 key.clone(),
1998 DetectorDataset {
1999 ds,
2000 frame_count: 0,
2001 frame_band: Vec::new(),
2002 },
2003 );
2004 }
2005
2006 if let Some(Hdf5Handle::Standard { detectors: d, .. }) = self.handle.as_mut() {
2007 *d = detectors;
2008 }
2009 self.open_data_type = Some(ds_data_type);
2010 self.open_frame_dims = Some(frame_dims);
2011 self.open_codec = array.codec.clone();
2012 Ok(())
2013 }
2014
2015 fn resolve_destination_key(&self, array: &NDArray) -> ADResult<String> {
2026 let default = format!("/{}", self.resolved_dataset_path);
2027 let attr_name = self
2028 .layout
2029 .as_ref()
2030 .and_then(|l| l.detector_data_destination.as_deref())
2031 .filter(|s| !s.is_empty());
2032 let Some(attr_name) = attr_name else {
2033 return Ok(default);
2034 };
2035 let Some(attr) = array.attributes.get(attr_name) else {
2038 return Ok(default);
2039 };
2040 match attr.value.as_string_typed() {
2041 Some(value) => {
2042 let known = matches!(
2043 &self.handle,
2044 Some(Hdf5Handle::Standard { detectors, .. })
2045 if detectors.contains_key(value)
2046 );
2047 Ok(if known { value.to_string() } else { default })
2048 }
2049 None => Err(ADError::UnsupportedConversion(format!(
2050 "HDF5 detector_data_destination attribute '{}' is not a string",
2051 attr_name
2052 ))),
2053 }
2054 }
2055
2056 fn write_standard(&mut self, array: &NDArray) -> ADResult<()> {
2061 if self.frame_count == 0 {
2062 self.create_detector_datasets(array)?;
2063 self.create_attribute_datasets(array);
2064 }
2065
2066 let frame_dims = self
2067 .open_frame_dims
2068 .clone()
2069 .ok_or_else(|| ADError::UnsupportedConversion("dataset not initialised".into()))?;
2070 let cur_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
2071 if cur_dims != frame_dims {
2072 return Err(ADError::UnsupportedConversion(format!(
2073 "HDF5 frame shape changed mid-stream: {:?} != {:?}",
2074 cur_dims, frame_dims
2075 )));
2076 }
2077
2078 if !codecs_match(self.open_codec.as_ref(), array.codec.as_ref()) {
2083 return Err(ADError::UnsupportedConversion(format!(
2084 "HDF5 codec changed mid-stream: dataset codec {:?} != frame codec {:?}",
2085 self.open_codec.as_ref().map(|c| c.name),
2086 array.codec.as_ref().map(|c| c.name),
2087 )));
2088 }
2089
2090 let (_shape, chunk, leading) = self.standard_layout(&frame_dims);
2091 let fc = chunk[0];
2092
2093 let dest_key = self.resolve_destination_key(array)?;
2099 let chunk_bytes = array.codec.as_ref().map(|codec| {
2100 let total_bytes =
2101 frame_dims.iter().product::<usize>() * codec.original_data_type.element_size();
2102 codec_chunk_bytes(codec, total_bytes, array.data.as_u8_slice())
2103 });
2104 let frame_le = if chunk_bytes.is_none() {
2105 Some(nd_buffer_to_le_bytes(&array.data))
2106 } else {
2107 None
2108 };
2109 let elem_size = array.data.data_type().element_size();
2110
2111 {
2112 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
2113 return Err(ADError::UnsupportedConversion(
2114 "HDF5 detector datasets not initialised".into(),
2115 ));
2116 };
2117 let det = detectors.get_mut(&dest_key).ok_or_else(|| {
2118 ADError::UnsupportedConversion(format!(
2119 "HDF5 destination dataset '{}' not found",
2120 dest_key
2121 ))
2122 })?;
2123
2124 let frame_idx = det.frame_count;
2127 if let Some(ref lead) = leading {
2128 let total: usize = lead.iter().product();
2129 if frame_idx >= total {
2130 return Err(ADError::UnsupportedConversion(format!(
2131 "HDF5 extra-dimension capacity exceeded: frame {} >= {}",
2132 frame_idx, total
2133 )));
2134 }
2135 }
2136
2137 if let Some(cb) = &chunk_bytes {
2138 det.ds.write_chunk_raw(frame_idx, cb, 0).map_err(|e| {
2144 ADError::UnsupportedConversion(format!("HDF5 direct chunk write error: {}", e))
2145 })?;
2146 } else {
2147 let fle = frame_le.expect("frame_le is set whenever chunk_bytes is None");
2153 det.frame_band.push(fle);
2154 if det.frame_band.len() >= fc {
2155 let leading_coords = match &leading {
2156 Some(lead) => Self::unravel(frame_idx, lead),
2157 None => vec![frame_idx / fc],
2158 };
2159 Self::flush_band(
2160 &det.ds,
2161 &leading_coords,
2162 &det.frame_band,
2163 &frame_dims,
2164 &chunk,
2165 elem_size,
2166 )?;
2167 det.frame_band.clear();
2168 }
2169 }
2170 det.frame_count += 1;
2171 }
2172
2173 if self.store_attributes {
2175 for ad in self.attr_datasets.iter_mut() {
2176 let value = array
2177 .attributes
2178 .get(&ad.name)
2179 .map(|a| a.value.clone())
2180 .unwrap_or(NDAttrValue::Undefined);
2181 ad.push(&value);
2182 }
2183 }
2184 Ok(())
2185 }
2186
2187 fn create_attribute_datasets(&mut self, array: &NDArray) {
2190 self.attr_datasets.clear();
2191 if !self.store_attributes {
2192 return;
2193 }
2194 for attr in array.attributes.iter() {
2195 self.attr_datasets.push(AttributeDataset::new(attr));
2196 }
2197 }
2198
2199 fn attribute_chunking(&self) -> usize {
2205 if self.chunk.ndattr_chunk != 0 {
2206 return self.chunk.ndattr_chunk;
2207 }
2208 if self.open_mode == NDFileMode::Single {
2209 return 1;
2210 }
2211 if self.num_capture > 0 {
2212 self.num_capture
2213 } else {
2214 16 * 1024
2215 }
2216 }
2217
2218 fn seed_ndattr_element_values(&mut self, array: &NDArray) {
2224 self.ndattr_first_values.clear();
2225 self.ndattr_last_values.clear();
2226 self.ndattr_element_names.clear();
2227 let Some(layout) = self.layout.as_ref() else {
2228 return;
2229 };
2230 let mut names: Vec<String> = Vec::new();
2231 for e in layout.ndattribute_element_attrs() {
2232 if !names.contains(&e.ndattribute) {
2233 names.push(e.ndattribute);
2234 }
2235 }
2236 for name in &names {
2237 if let Some(attr) = array.attributes.get(name) {
2238 self.ndattr_first_values
2239 .insert(name.clone(), attr.value.clone());
2240 self.ndattr_last_values
2241 .insert(name.clone(), attr.value.clone());
2242 }
2243 }
2244 self.ndattr_element_names = names;
2245 }
2246
2247 fn update_ndattr_element_values(&mut self, array: &NDArray) {
2253 if self.ndattr_element_names.is_empty() {
2254 return;
2255 }
2256 for name in &self.ndattr_element_names {
2257 if let Some(attr) = array.attributes.get(name) {
2258 self.ndattr_last_values
2259 .insert(name.clone(), attr.value.clone());
2260 }
2261 }
2262 }
2263
2264 fn flush_ndattr_element_attrs(
2281 &self,
2282 file: &H5File,
2283 detectors: &std::collections::HashMap<String, DetectorDataset>,
2284 ) -> ADResult<()> {
2285 use crate::hdf5_layout::LayoutWhen;
2286 let Some(layout) = self.layout.as_ref() else {
2287 return Ok(());
2288 };
2289 for e in layout.ndattribute_element_attrs() {
2290 let value = match e.when {
2291 LayoutWhen::OnFileOpen | LayoutWhen::OnFrame => {
2292 self.ndattr_first_values.get(&e.ndattribute)
2293 }
2294 LayoutWhen::OnFileClose => self.ndattr_last_values.get(&e.ndattribute),
2295 LayoutWhen::OnFileWrite => None,
2296 };
2297 let Some(value) = value else {
2298 continue;
2299 };
2300 let path = e.element_path.trim_start_matches('/');
2301 if e.is_dataset {
2302 if let Some(det) = detectors.get(&e.element_path) {
2309 write_ndattr_dataset_attr(&det.ds, &e.attr_name, value);
2310 } else if let Ok(ds) = file.dataset_writer(path) {
2311 write_ndattr_dataset_attr(&ds, &e.attr_name, value);
2312 }
2313 } else {
2314 let group = Self::open_write_group(file, path)?;
2315 write_ndattr_group_attr(&group, &e.attr_name, value);
2316 }
2317 }
2318 Ok(())
2319 }
2320
2321 fn write_swmr_ndarray_default_attrs(
2330 &self,
2331 swmr: &mut SwmrFileWriter,
2332 ds_index: usize,
2333 array: &NDArray,
2334 ) -> ADResult<()> {
2335 let nd_num_dims = array.dims.len() as i32;
2336 swmr.set_dataset_attr_numeric(ds_index, "NDArrayNumDims", &nd_num_dims)
2337 .map_err(|e| {
2338 ADError::UnsupportedConversion(format!("SWMR NDArrayNumDims attr: {}", e))
2339 })?;
2340 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
2341 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
2342 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
2343 for (name, vals) in [
2344 ("NDArrayDimOffset", &dim_offsets),
2345 ("NDArrayDimBinning", &dim_binnings),
2346 ("NDArrayDimReverse", &dim_reverses),
2347 ] {
2348 let res = if vals.len() == 1 {
2349 swmr.set_dataset_attr_numeric(ds_index, name, &vals[0])
2350 } else {
2351 swmr.set_dataset_attr_array(ds_index, name, &[vals.len() as u64], vals)
2352 };
2353 res.map_err(|e| ADError::UnsupportedConversion(format!("SWMR {} attr: {}", name, e)))?;
2354 }
2355 Ok(())
2356 }
2357
2358 fn write_swmr_ndattr_element_attrs(
2369 &self,
2370 swmr: &mut SwmrFileWriter,
2371 ds_index: usize,
2372 ) -> ADResult<()> {
2373 use crate::hdf5_layout::LayoutWhen;
2374 let Some(layout) = self.layout.as_ref() else {
2375 return Ok(());
2376 };
2377 for e in layout.ndattribute_element_attrs() {
2378 if !matches!(e.when, LayoutWhen::OnFileOpen | LayoutWhen::OnFrame) {
2379 continue;
2380 }
2381 let Some(value) = self.ndattr_first_values.get(&e.ndattribute) else {
2382 continue;
2383 };
2384 if e.is_dataset {
2385 if e.element_path.trim_start_matches('/') == self.resolved_dataset_path {
2389 write_swmr_ndattr_dataset_attr(swmr, ds_index, &e.attr_name, value)?;
2390 }
2391 } else {
2392 write_swmr_ndattr_group_attr(swmr, &e.element_path, &e.attr_name, value)?;
2393 }
2394 }
2395 Ok(())
2396 }
2397
2398 fn flush_attribute_datasets(&mut self) -> ADResult<()> {
2401 if self.attr_datasets.is_empty() {
2402 return Ok(());
2403 }
2404 let chunk_depth = self.attribute_chunking();
2405 let ndattr_group = self.resolved_ndattr_group.clone();
2406 let targets: Vec<(String, String)> = self
2410 .attr_datasets
2411 .iter()
2412 .map(|ad| {
2413 match self
2414 .layout
2415 .as_ref()
2416 .and_then(|l| l.ndattribute_dataset(&ad.name))
2417 {
2418 Some((g, name)) => (g.trim_start_matches('/').to_string(), name),
2419 None => (ndattr_group.clone(), ad.name.clone()),
2420 }
2421 })
2422 .collect();
2423 let h5file = match self.handle {
2424 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2425 _ => return Ok(()),
2426 };
2427 let mut group_cache: std::collections::HashMap<String, rust_hdf5::H5Group> =
2431 std::collections::HashMap::new();
2432
2433 for (ad, (group_path, ds_name)) in self.attr_datasets.iter().zip(targets.iter()) {
2434 if ad.frames == 0 {
2435 continue;
2436 }
2437 let n = ad.frames;
2438 let chunk = chunk_depth;
2442
2443 if !group_cache.contains_key(group_path) {
2444 let g = if group_path.is_empty() {
2445 h5file.create_group("NDAttributes").map_err(|e| {
2446 ADError::UnsupportedConversion(format!("HDF5 group error: {}", e))
2447 })?
2448 } else {
2449 Self::open_write_group(h5file, group_path)?
2450 };
2451 group_cache.insert(group_path.clone(), g);
2452 }
2453 let group = &group_cache[group_path];
2454
2455 macro_rules! create_attr_ds {
2456 ($t:ty) => {{
2457 let es = std::mem::size_of::<$t>();
2458 let ds = group
2459 .new_dataset::<$t>()
2460 .shape(&[n])
2461 .chunk(&[chunk])
2462 .max_shape(&[None])
2463 .create(ds_name)
2464 .map_err(|e| {
2465 ADError::UnsupportedConversion(format!(
2466 "HDF5 attribute dataset error: {}",
2467 e
2468 ))
2469 })?;
2470 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2474 ds
2475 }};
2476 }
2477
2478 let ds = match ad.data_type {
2479 NDAttrDataType::Int8 => create_attr_ds!(i8),
2480 NDAttrDataType::UInt8 => create_attr_ds!(u8),
2481 NDAttrDataType::Int16 => create_attr_ds!(i16),
2482 NDAttrDataType::UInt16 => create_attr_ds!(u16),
2483 NDAttrDataType::Int32 => create_attr_ds!(i32),
2484 NDAttrDataType::UInt32 => create_attr_ds!(u32),
2485 NDAttrDataType::Int64 => create_attr_ds!(i64),
2486 NDAttrDataType::UInt64 => create_attr_ds!(u64),
2487 NDAttrDataType::Float32 => create_attr_ds!(f32),
2488 NDAttrDataType::Float64 => create_attr_ds!(f64),
2489 NDAttrDataType::String => {
2490 let es = MAX_ATTRIBUTE_STRING_SIZE;
2497 let ds = group
2498 .new_dataset::<FixedStr256>()
2499 .shape([n])
2500 .chunk(&[chunk])
2501 .max_shape(&[None])
2502 .create(&ad.name)
2503 .map_err(|e| {
2504 ADError::UnsupportedConversion(format!(
2505 "HDF5 attribute dataset error: {}",
2506 e
2507 ))
2508 })?;
2509 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2510 ds
2511 }
2512 };
2513
2514 write_ndattr_descriptors(&ds, ad);
2518 }
2519 Ok(())
2520 }
2521
2522 fn flush_performance_dataset(&mut self) -> ADResult<()> {
2525 if !self.store_performance || self.perf_rows.is_empty() {
2526 return Ok(());
2527 }
2528 let n = self.perf_rows.len();
2529 let mut flat: Vec<f64> = Vec::with_capacity(n * 5);
2530 for row in &self.perf_rows {
2531 flat.extend_from_slice(row);
2532 }
2533 let raw: Vec<u8> = flat.iter().flat_map(|v| v.to_le_bytes()).collect();
2536
2537 let chunking = self.attribute_chunking();
2541 let perf_group = self.resolved_perf_group.clone();
2542 let h5file = match self.handle {
2543 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2544 _ => return Ok(()),
2545 };
2546 let group = if perf_group.is_empty() {
2551 h5file
2552 .create_group("performance")
2553 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
2554 } else {
2555 Self::open_write_group(h5file, &perf_group)?
2556 };
2557 let ds = group
2558 .new_dataset::<f64>()
2559 .shape([n, 5])
2560 .chunk(&[chunking, 5])
2561 .max_shape(&[None, Some(5)])
2562 .create("timestamp")
2563 .map_err(|e| {
2564 ADError::UnsupportedConversion(format!("HDF5 performance dataset error: {}", e))
2565 })?;
2566 write_chunked_buffer(&ds, &raw, chunking * 5 * 8)?;
2569 Ok(())
2570 }
2571
2572 fn write_swmr(&mut self, array: &NDArray) -> ADResult<()> {
2574 if array.codec.is_some() {
2583 return Err(ADError::UnsupportedConversion(
2584 "HDF5 SWMR mode cannot write a pre-compressed array (no raw-chunk \
2585 append in the SWMR backend); use standard capture mode for \
2586 direct chunk write"
2587 .into(),
2588 ));
2589 }
2590
2591 let frame_idx = self.frame_count;
2594
2595 let (writer, ds_index, grid) = match self.handle {
2596 Some(Hdf5Handle::Swmr {
2597 ref mut writer,
2598 ds_index,
2599 ref grid,
2600 ..
2601 }) => (writer, ds_index, grid),
2602 _ => return Err(ADError::UnsupportedConversion("no SWMR writer open".into())),
2603 };
2604
2605 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
2610 match grid {
2611 Some(g) => {
2615 let leading_coords = Self::unravel(frame_idx, &g.leading);
2616 for (coords, tile) in Self::band_chunk_writes(
2617 &leading_coords,
2618 std::slice::from_ref(&frame_bytes),
2619 &g.frame_dims,
2620 &g.chunk,
2621 g.elem_size,
2622 ) {
2623 let coords_u64: Vec<u64> = coords.iter().map(|&c| c as u64).collect();
2624 writer
2625 .write_chunk_at(ds_index, &coords_u64, &tile)
2626 .map_err(|e| {
2627 ADError::UnsupportedConversion(format!(
2628 "SWMR write_chunk_at error: {}",
2629 e
2630 ))
2631 })?;
2632 }
2633 }
2634 None => {
2635 writer.append_frame(ds_index, &frame_bytes).map_err(|e| {
2636 ADError::UnsupportedConversion(format!("SWMR append error: {}", e))
2637 })?;
2638 }
2639 }
2640
2641 let count = self.frame_count + 1; if self.flush_nth_frame > 0 && count % self.flush_nth_frame == 0 {
2644 writer
2645 .flush()
2646 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR flush error: {}", e)))?;
2647 }
2648 Ok(())
2649 }
2650
2651 fn record_performance(&mut self, write_duration: f64, frame_bytes: usize) {
2653 let now = std::time::Instant::now();
2654 let first = *self.perf_first.get_or_insert(now);
2655 let runtime = now.duration_since(first).as_secs_f64();
2656 let period = match self.perf_prev {
2657 Some(prev) => now.duration_since(prev).as_secs_f64(),
2658 None => write_duration,
2659 };
2660 self.perf_prev = Some(now);
2661 let fb = frame_bytes as f64;
2662 let inst_speed = if period > 0.0 { fb / period } else { 0.0 };
2663 let avg_speed = if runtime > 0.0 {
2664 (self.perf_rows.len() as f64 + 1.0) * fb / runtime
2665 } else {
2666 0.0
2667 };
2668 self.perf_rows
2669 .push([write_duration, period, runtime, inst_speed, avg_speed]);
2670 }
2671}
2672
2673fn codecs_match(a: Option<&Codec>, b: Option<&Codec>) -> bool {
2680 match (a, b) {
2681 (None, None) => true,
2682 (Some(x), Some(y)) => {
2683 x.name == y.name
2684 && x.level == y.level
2685 && x.shuffle == y.shuffle
2686 && x.compressor == y.compressor
2687 }
2688 _ => false,
2689 }
2690}
2691
2692fn codec_chunk_bytes(codec: &Codec, uncompressed_total_bytes: usize, payload: &[u8]) -> Vec<u8> {
2714 match codec.name {
2715 CodecName::LZ4 => {
2716 let mut out = Vec::with_capacity(16 + payload.len());
2717 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2718 out.extend_from_slice(&(uncompressed_total_bytes as u32).to_be_bytes());
2719 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
2720 out.extend_from_slice(payload);
2721 out
2722 }
2723 CodecName::BSLZ4 => {
2724 let elem_size = codec.original_data_type.element_size();
2725 let block_bytes = crate::codec::bshuf_default_block_size(elem_size) * elem_size;
2726 let mut out = Vec::with_capacity(12 + payload.len());
2727 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2728 out.extend_from_slice(&(block_bytes as u32).to_be_bytes());
2729 out.extend_from_slice(payload);
2730 out
2731 }
2732 _ => payload.to_vec(),
2733 }
2734}
2735
2736fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
2748 match buf {
2749 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
2750 NDDataBuffer::U8(v) => v.clone(),
2751 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2752 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2753 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2754 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2755 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2756 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2757 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2758 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2759 }
2760}
2761
2762fn write_chunked_buffer(
2766 ds: &rust_hdf5::H5Dataset,
2767 buffer: &[u8],
2768 chunk_bytes: usize,
2769) -> ADResult<()> {
2770 let n_chunks = buffer.len().div_ceil(chunk_bytes.max(1));
2771 for c in 0..n_chunks {
2772 let start = c * chunk_bytes;
2773 let end = ((c + 1) * chunk_bytes).min(buffer.len());
2774 let slice = &buffer[start..end];
2775 if slice.len() == chunk_bytes {
2776 ds.write_chunk(c, slice)
2777 } else {
2778 let mut padded = vec![0u8; chunk_bytes];
2779 padded[..slice.len()].copy_from_slice(slice);
2780 ds.write_chunk(c, &padded)
2781 }
2782 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 chunk write error: {}", e)))?;
2783 }
2784 Ok(())
2785}
2786
2787fn write_ndattr_descriptors(ds: &rust_hdf5::H5Dataset, ad: &AttributeDataset) {
2793 for (aname, avalue) in [
2794 ("NDAttrName", ad.name.as_str()),
2795 ("NDAttrDescription", ad.description.as_str()),
2796 ("NDAttrSourceType", ad.source_type.as_str()),
2797 ("NDAttrSource", ad.source.as_str()),
2798 ] {
2799 if avalue.is_empty() {
2800 continue;
2801 }
2802 let s = rust_hdf5::types::VarLenUnicode(avalue.to_string());
2803 let _ = ds
2804 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2805 .shape(())
2806 .create(aname)
2807 .and_then(|a| a.write_scalar(&s));
2808 }
2809}
2810
2811fn write_ndattr_group_attr(group: &rust_hdf5::H5Group, attr_name: &str, value: &NDAttrValue) {
2817 macro_rules! num {
2818 ($v:expr) => {{
2819 let _ = group.set_attr_numeric(attr_name, &$v);
2820 }};
2821 }
2822 match value {
2823 NDAttrValue::Int8(v) => num!(*v),
2824 NDAttrValue::UInt8(v) => num!(*v),
2825 NDAttrValue::Int16(v) => num!(*v),
2826 NDAttrValue::UInt16(v) => num!(*v),
2827 NDAttrValue::Int32(v) => num!(*v),
2828 NDAttrValue::UInt32(v) => num!(*v),
2829 NDAttrValue::Int64(v) => num!(*v),
2830 NDAttrValue::UInt64(v) => num!(*v),
2831 NDAttrValue::Float32(v) => num!(*v),
2832 NDAttrValue::Float64(v) => num!(*v),
2833 NDAttrValue::String(s) => {
2834 let _ = group.set_attr_string(attr_name, s);
2835 }
2836 NDAttrValue::Undefined => {}
2837 }
2838}
2839
2840fn write_ndattr_dataset_attr(ds: &rust_hdf5::H5Dataset, attr_name: &str, value: &NDAttrValue) {
2847 macro_rules! num {
2848 ($v:expr, $t:ty) => {{
2849 let v: $t = $v;
2850 let _ = ds
2851 .new_attr::<$t>()
2852 .shape(())
2853 .create(attr_name)
2854 .and_then(|a| a.write_numeric(&v));
2855 }};
2856 }
2857 match value {
2858 NDAttrValue::Int8(v) => num!(*v, i8),
2859 NDAttrValue::UInt8(v) => num!(*v, u8),
2860 NDAttrValue::Int16(v) => num!(*v, i16),
2861 NDAttrValue::UInt16(v) => num!(*v, u16),
2862 NDAttrValue::Int32(v) => num!(*v, i32),
2863 NDAttrValue::UInt32(v) => num!(*v, u32),
2864 NDAttrValue::Int64(v) => num!(*v, i64),
2865 NDAttrValue::UInt64(v) => num!(*v, u64),
2866 NDAttrValue::Float32(v) => num!(*v, f32),
2867 NDAttrValue::Float64(v) => num!(*v, f64),
2868 NDAttrValue::String(s) => {
2869 let sv = rust_hdf5::types::VarLenUnicode(s.clone());
2870 let _ = ds
2871 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2872 .shape(())
2873 .create(attr_name)
2874 .and_then(|a| a.write_scalar(&sv));
2875 }
2876 NDAttrValue::Undefined => {}
2877 }
2878}
2879
2880fn write_swmr_ndattr_group_attr(
2885 swmr: &mut SwmrFileWriter,
2886 group_path: &str,
2887 attr_name: &str,
2888 value: &NDAttrValue,
2889) -> ADResult<()> {
2890 let res = match value {
2891 NDAttrValue::Int8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2892 NDAttrValue::UInt8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2893 NDAttrValue::Int16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2894 NDAttrValue::UInt16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2895 NDAttrValue::Int32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2896 NDAttrValue::UInt32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2897 NDAttrValue::Int64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2898 NDAttrValue::UInt64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2899 NDAttrValue::Float32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2900 NDAttrValue::Float64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2901 NDAttrValue::String(s) => swmr.set_group_attr_string(group_path, attr_name, s),
2902 NDAttrValue::Undefined => return Ok(()),
2903 };
2904 res.map_err(|e| {
2905 ADError::UnsupportedConversion(format!(
2906 "SWMR ndattribute group attribute '{}/{}': {}",
2907 group_path, attr_name, e
2908 ))
2909 })
2910}
2911
2912fn write_swmr_ndattr_dataset_attr(
2919 swmr: &mut SwmrFileWriter,
2920 ds_index: usize,
2921 attr_name: &str,
2922 value: &NDAttrValue,
2923) -> ADResult<()> {
2924 let res = match value {
2925 NDAttrValue::Int8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2926 NDAttrValue::UInt8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2927 NDAttrValue::Int16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2928 NDAttrValue::UInt16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2929 NDAttrValue::Int32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2930 NDAttrValue::UInt32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2931 NDAttrValue::Int64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2932 NDAttrValue::UInt64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2933 NDAttrValue::Float32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2934 NDAttrValue::Float64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2935 NDAttrValue::String(s) => swmr.set_dataset_attr_string(ds_index, attr_name, s),
2936 NDAttrValue::Undefined => return Ok(()),
2937 };
2938 res.map_err(|e| {
2939 ADError::UnsupportedConversion(format!(
2940 "SWMR ndattribute dataset attribute '{}': {}",
2941 attr_name, e
2942 ))
2943 })
2944}
2945
2946fn write_group_constant_attrs(
2952 group: &rust_hdf5::H5Group,
2953 attrs: &[crate::hdf5_layout::LayoutAttribute],
2954) {
2955 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2956 for a in attrs {
2957 if a.source != LayoutSource::Constant {
2958 continue;
2959 }
2960 let _ = match a.data_type {
2961 LayoutDataType::Int => {
2962 group.set_attr_numeric(&a.name, &a.value.trim().parse::<i64>().unwrap_or(0))
2963 }
2964 LayoutDataType::Float => {
2965 group.set_attr_numeric(&a.name, &a.value.trim().parse::<f64>().unwrap_or(0.0))
2966 }
2967 LayoutDataType::String => group.set_attr_string(&a.name, &a.value),
2968 };
2969 }
2970}
2971
2972fn write_swmr_group_constant_attrs(
2976 swmr: &mut SwmrFileWriter,
2977 group_path: &str,
2978 attrs: &[crate::hdf5_layout::LayoutAttribute],
2979) -> ADResult<()> {
2980 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2981 for a in attrs {
2982 if a.source != LayoutSource::Constant {
2983 continue;
2984 }
2985 match a.data_type {
2986 LayoutDataType::Int => swmr.set_group_attr_numeric(
2987 group_path,
2988 &a.name,
2989 &a.value.trim().parse::<i64>().unwrap_or(0),
2990 ),
2991 LayoutDataType::Float => swmr.set_group_attr_numeric(
2992 group_path,
2993 &a.name,
2994 &a.value.trim().parse::<f64>().unwrap_or(0.0),
2995 ),
2996 LayoutDataType::String => swmr.set_group_attr_string(group_path, &a.name, &a.value),
2997 }
2998 .map_err(|e| {
2999 ADError::UnsupportedConversion(format!(
3000 "SWMR layout group attribute '{}/{}': {}",
3001 group_path, a.name, e
3002 ))
3003 })?;
3004 }
3005 Ok(())
3006}
3007
3008impl Default for Hdf5Writer {
3009 fn default() -> Self {
3010 Self::new()
3011 }
3012}
3013
3014impl NDFileWriter for Hdf5Writer {
3015 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
3016 self.current_path = Some(path.to_path_buf());
3017 self.open_mode = mode;
3018 self.frame_count = 0;
3019 self.total_runtime = 0.0;
3020 self.total_bytes = 0;
3021 self.swmr_cb_counter = 0;
3022 self.open_data_type = None;
3023 self.open_frame_dims = None;
3024 self.open_codec = None;
3025 self.perf_rows.clear();
3026 self.perf_prev = None;
3027 self.perf_first = None;
3028 self.attr_datasets.clear();
3029 self.resolve_layout_paths();
3032 self.seed_ndattr_element_values(array);
3035
3036 if self.swmr_mode && mode == NDFileMode::Stream {
3037 self.open_swmr(path, array)
3038 } else {
3039 let h5file = H5File::create(path)
3040 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 create error: {}", e)))?;
3041 self.handle = Some(Hdf5Handle::Standard {
3042 file: h5file,
3043 detectors: std::collections::HashMap::new(),
3044 });
3045 Ok(())
3046 }
3047 }
3048
3049 fn set_num_capture(&mut self, n: usize) {
3050 self.num_capture = n;
3051 }
3052
3053 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
3054 let start = std::time::Instant::now();
3055
3056 let is_swmr = matches!(self.handle, Some(Hdf5Handle::Swmr { .. }));
3057 if is_swmr {
3058 self.write_swmr(array)?;
3059 } else {
3060 self.write_standard(array)?;
3061 }
3062 self.update_ndattr_element_values(array);
3065 self.frame_count += 1;
3066
3067 let elapsed = start.elapsed().as_secs_f64();
3068 let frame_bytes = array.data.as_u8_slice().len();
3069 if self.store_performance {
3070 self.total_runtime += elapsed;
3071 self.total_bytes += frame_bytes as u64;
3072 self.record_performance(elapsed, frame_bytes);
3073 }
3074 Ok(())
3075 }
3076
3077 fn read_file(&mut self) -> ADResult<NDArray> {
3078 self.resolve_layout_paths();
3082 let dataset_path = self.resolved_dataset_path.clone();
3083 let path = self
3084 .current_path
3085 .as_ref()
3086 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
3087
3088 let h5file = H5File::open(path)
3089 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 open error: {}", e)))?;
3090
3091 let ds = h5file
3092 .dataset(&dataset_path)
3093 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e)))?;
3094
3095 let shape = ds.shape();
3096 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
3097 let element_size = ds.element_size();
3098
3099 let recorded: Option<NDDataType> = ds
3101 .attr(DTYPE_ATTR)
3102 .ok()
3103 .and_then(|a| a.read_numeric::<i32>().ok())
3104 .and_then(|v| NDDataType::from_ordinal(v as u8));
3105
3106 let data_type = recorded.unwrap_or(match element_size {
3107 1 => NDDataType::UInt8,
3108 2 => NDDataType::UInt16,
3109 4 => NDDataType::Float32,
3110 8 => NDDataType::Float64,
3111 other => {
3112 return Err(ADError::UnsupportedConversion(format!(
3113 "unsupported HDF5 element size {}",
3114 other
3115 )));
3116 }
3117 });
3118
3119 macro_rules! read_typed {
3120 ($t:ty, $variant:ident) => {{
3121 let data = ds.read_raw::<$t>().map_err(|e| {
3122 ADError::UnsupportedConversion(format!("HDF5 read error: {}", e))
3123 })?;
3124 let mut arr = NDArray::new(dims, data_type);
3125 arr.data = NDDataBuffer::$variant(data);
3126 return Ok(arr);
3127 }};
3128 }
3129
3130 match data_type {
3131 NDDataType::Int8 => read_typed!(i8, I8),
3132 NDDataType::UInt8 => read_typed!(u8, U8),
3133 NDDataType::Int16 => read_typed!(i16, I16),
3134 NDDataType::UInt16 => read_typed!(u16, U16),
3135 NDDataType::Int32 => read_typed!(i32, I32),
3136 NDDataType::UInt32 => read_typed!(u32, U32),
3137 NDDataType::Int64 => read_typed!(i64, I64),
3138 NDDataType::UInt64 => read_typed!(u64, U64),
3139 NDDataType::Float32 => read_typed!(f32, F32),
3140 NDDataType::Float64 => read_typed!(f64, F64),
3141 }
3142 }
3143
3144 fn close_file(&mut self) -> ADResult<()> {
3145 match self.handle {
3146 Some(Hdf5Handle::Standard { .. }) => {
3147 self.finalize_standard_datasets()?;
3151 self.flush_attribute_datasets()?;
3152 self.flush_performance_dataset()?;
3153 match self.handle {
3156 Some(Hdf5Handle::Standard {
3157 ref file,
3158 ref detectors,
3159 }) => {
3160 self.build_layout_hardlinks(file)?;
3161 self.flush_ndattr_element_attrs(file, detectors)?;
3165 }
3166 _ => unreachable!("handle is Standard in this arm"),
3167 }
3168 if let Some(Hdf5Handle::Standard { file, detectors }) = self.handle.take() {
3174 if self.fsync_on_close {
3175 drop(file);
3176 } else {
3177 file.close_no_sync().map_err(|e| {
3178 ADError::UnsupportedConversion(format!(
3179 "HDF5 close_no_sync error: {}",
3180 e
3181 ))
3182 })?;
3183 }
3184 drop(detectors);
3185 }
3186 }
3187 Some(Hdf5Handle::Swmr { .. }) => {
3188 if let Some(Hdf5Handle::Swmr { mut writer, .. }) = self.handle.take() {
3195 writer.flush().map_err(|e| {
3202 ADError::UnsupportedConversion(format!("SWMR flush-on-close error: {}", e))
3203 })?;
3204 writer.close().map_err(|e| {
3205 ADError::UnsupportedConversion(format!("SWMR close error: {}", e))
3206 })?;
3207 }
3208 }
3209 None => {}
3210 }
3211 self.current_path = None;
3212 Ok(())
3213 }
3214
3215 fn supports_multiple_arrays(&self) -> bool {
3216 true
3217 }
3218}
3219
3220#[derive(Default)]
3226struct Hdf5ParamIndices {
3227 compression_type: Option<usize>,
3228 z_compress_level: Option<usize>,
3229 szip_num_pixels: Option<usize>,
3230 nbit_precision: Option<usize>,
3231 nbit_offset: Option<usize>,
3232 jpeg_quality: Option<usize>,
3233 blosc_shuffle_type: Option<usize>,
3234 blosc_compressor: Option<usize>,
3235 blosc_compress_level: Option<usize>,
3236 store_attributes: Option<usize>,
3237 store_performance: Option<usize>,
3238 total_runtime: Option<usize>,
3239 total_io_speed: Option<usize>,
3240 swmr_mode: Option<usize>,
3241 swmr_flush_now: Option<usize>,
3242 swmr_running: Option<usize>,
3243 swmr_cb_counter: Option<usize>,
3244 swmr_supported: Option<usize>,
3245 flush_nth_frame: Option<usize>,
3246 chunk_size_auto: Option<usize>,
3247 n_row_chunks: Option<usize>,
3248 n_col_chunks: Option<usize>,
3249 n_frames_chunks: Option<usize>,
3250 ndattr_chunk: Option<usize>,
3251 n_extra_dims: Option<usize>,
3252 extra_dim_size: [Option<usize>; MAX_EXTRA_DIMS],
3253 extra_dim_name: [Option<usize>; MAX_EXTRA_DIMS],
3254 fill_value: Option<usize>,
3255 dim_att_datasets: Option<usize>,
3256 layout_filename: Option<usize>,
3257 layout_valid: Option<usize>,
3258 layout_error_msg: Option<usize>,
3259}
3260
3261pub struct Hdf5FileProcessor {
3263 ctrl: FilePluginController<Hdf5Writer>,
3264 hdf5_params: Hdf5ParamIndices,
3265}
3266
3267impl Hdf5FileProcessor {
3268 pub fn new() -> Self {
3269 Self {
3270 ctrl: FilePluginController::new(Hdf5Writer::new()),
3271 hdf5_params: Hdf5ParamIndices::default(),
3272 }
3273 }
3274
3275 pub fn set_dataset_name(&mut self, name: &str) {
3276 self.ctrl.writer.set_dataset_name(name);
3277 }
3278}
3279
3280fn register_hdf5_params(
3282 base: &mut asyn_rs::port::PortDriverBase,
3283) -> asyn_rs::error::AsynResult<()> {
3284 use asyn_rs::param::ParamType;
3285 base.create_param("HDF5_SWMRFlushNow", ParamType::Int32)?;
3286 base.create_param("HDF5_chunkSizeAuto", ParamType::Int32)?;
3287 base.create_param("HDF5_nRowChunks", ParamType::Int32)?;
3288 base.create_param("HDF5_nColChunks", ParamType::Int32)?;
3289 base.create_param("HDF5_chunkSize2", ParamType::Int32)?;
3290 base.create_param("HDF5_chunkSize3", ParamType::Int32)?;
3291 base.create_param("HDF5_chunkSize4", ParamType::Int32)?;
3292 base.create_param("HDF5_chunkSize5", ParamType::Int32)?;
3293 base.create_param("HDF5_chunkSize6", ParamType::Int32)?;
3294 base.create_param("HDF5_chunkSize7", ParamType::Int32)?;
3295 base.create_param("HDF5_chunkSize8", ParamType::Int32)?;
3296 base.create_param("HDF5_chunkSize9", ParamType::Int32)?;
3297 base.create_param("HDF5_nFramesChunks", ParamType::Int32)?;
3298 base.create_param("HDF5_NDAttributeChunk", ParamType::Int32)?;
3299 base.create_param("HDF5_chunkBoundaryAlign", ParamType::Int32)?;
3300 base.create_param("HDF5_chunkBoundaryThreshold", ParamType::Int32)?;
3301 base.create_param("HDF5_nExtraDims", ParamType::Int32)?;
3302 base.create_param("HDF5_extraDimSizeN", ParamType::Int32)?;
3303 base.create_param("HDF5_extraDimNameN", ParamType::Octet)?;
3304 base.create_param("HDF5_extraDimSizeX", ParamType::Int32)?;
3305 base.create_param("HDF5_extraDimNameX", ParamType::Octet)?;
3306 base.create_param("HDF5_extraDimSizeY", ParamType::Int32)?;
3307 base.create_param("HDF5_extraDimNameY", ParamType::Octet)?;
3308 base.create_param("HDF5_extraDimSize3", ParamType::Int32)?;
3309 base.create_param("HDF5_extraDimName3", ParamType::Octet)?;
3310 base.create_param("HDF5_extraDimSize4", ParamType::Int32)?;
3311 base.create_param("HDF5_extraDimName4", ParamType::Octet)?;
3312 base.create_param("HDF5_extraDimSize5", ParamType::Int32)?;
3313 base.create_param("HDF5_extraDimName5", ParamType::Octet)?;
3314 base.create_param("HDF5_extraDimSize6", ParamType::Int32)?;
3315 base.create_param("HDF5_extraDimName6", ParamType::Octet)?;
3316 base.create_param("HDF5_extraDimSize7", ParamType::Int32)?;
3317 base.create_param("HDF5_extraDimName7", ParamType::Octet)?;
3318 base.create_param("HDF5_extraDimSize8", ParamType::Int32)?;
3319 base.create_param("HDF5_extraDimName8", ParamType::Octet)?;
3320 base.create_param("HDF5_extraDimSize9", ParamType::Int32)?;
3321 base.create_param("HDF5_extraDimName9", ParamType::Octet)?;
3322 base.create_param("HDF5_storeAttributes", ParamType::Int32)?;
3323 base.create_param("HDF5_storePerformance", ParamType::Int32)?;
3324 base.create_param("HDF5_totalRuntime", ParamType::Float64)?;
3325 base.create_param("HDF5_totalIoSpeed", ParamType::Float64)?;
3326 base.create_param("HDF5_flushNthFrame", ParamType::Int32)?;
3327 base.create_param("HDF5_compressionType", ParamType::Int32)?;
3328 base.create_param("HDF5_nbitsPrecision", ParamType::Int32)?;
3329 base.create_param("HDF5_nbitsOffset", ParamType::Int32)?;
3330 base.create_param("HDF5_szipNumPixels", ParamType::Int32)?;
3331 base.create_param("HDF5_zCompressLevel", ParamType::Int32)?;
3332 base.create_param("HDF5_bloscShuffleType", ParamType::Int32)?;
3333 base.create_param("HDF5_bloscCompressor", ParamType::Int32)?;
3334 base.create_param("HDF5_bloscCompressLevel", ParamType::Int32)?;
3335 base.create_param("HDF5_jpegQuality", ParamType::Int32)?;
3336 base.create_param("HDF5_dimAttDatasets", ParamType::Int32)?;
3337 base.create_param("HDF5_layoutErrorMsg", ParamType::Octet)?;
3338 base.create_param("HDF5_layoutValid", ParamType::Int32)?;
3339 base.create_param("HDF5_layoutFilename", ParamType::Octet)?;
3340 base.create_param("HDF5_SWMRSupported", ParamType::Int32)?;
3341 base.create_param("HDF5_SWMRMode", ParamType::Int32)?;
3342 base.create_param("HDF5_SWMRRunning", ParamType::Int32)?;
3343 base.create_param("HDF5_SWMRCbCounter", ParamType::Int32)?;
3344 base.create_param("HDF5_posRunning", ParamType::Int32)?;
3345 base.create_param("HDF5_posNameDimN", ParamType::Octet)?;
3346 base.create_param("HDF5_posNameDimX", ParamType::Octet)?;
3347 base.create_param("HDF5_posNameDimY", ParamType::Octet)?;
3348 base.create_param("HDF5_posNameDim3", ParamType::Octet)?;
3349 base.create_param("HDF5_posNameDim4", ParamType::Octet)?;
3350 base.create_param("HDF5_posNameDim5", ParamType::Octet)?;
3351 base.create_param("HDF5_posNameDim6", ParamType::Octet)?;
3352 base.create_param("HDF5_posNameDim7", ParamType::Octet)?;
3353 base.create_param("HDF5_posNameDim8", ParamType::Octet)?;
3354 base.create_param("HDF5_posNameDim9", ParamType::Octet)?;
3355 base.create_param("HDF5_posIndexDimN", ParamType::Octet)?;
3356 base.create_param("HDF5_posIndexDimX", ParamType::Octet)?;
3357 base.create_param("HDF5_posIndexDimY", ParamType::Octet)?;
3358 base.create_param("HDF5_posIndexDim3", ParamType::Octet)?;
3359 base.create_param("HDF5_posIndexDim4", ParamType::Octet)?;
3360 base.create_param("HDF5_posIndexDim5", ParamType::Octet)?;
3361 base.create_param("HDF5_posIndexDim6", ParamType::Octet)?;
3362 base.create_param("HDF5_posIndexDim7", ParamType::Octet)?;
3363 base.create_param("HDF5_posIndexDim8", ParamType::Octet)?;
3364 base.create_param("HDF5_posIndexDim9", ParamType::Octet)?;
3365 base.create_param("HDF5_fillValue", ParamType::Float64)?;
3366 base.create_param("HDF5_extraDimChunkX", ParamType::Int32)?;
3367 base.create_param("HDF5_extraDimChunkY", ParamType::Int32)?;
3368 base.create_param("HDF5_extraDimChunk3", ParamType::Int32)?;
3369 base.create_param("HDF5_extraDimChunk4", ParamType::Int32)?;
3370 base.create_param("HDF5_extraDimChunk5", ParamType::Int32)?;
3371 base.create_param("HDF5_extraDimChunk6", ParamType::Int32)?;
3372 base.create_param("HDF5_extraDimChunk7", ParamType::Int32)?;
3373 base.create_param("HDF5_extraDimChunk8", ParamType::Int32)?;
3374 base.create_param("HDF5_extraDimChunk9", ParamType::Int32)?;
3375 Ok(())
3376}
3377
3378impl Default for Hdf5FileProcessor {
3379 fn default() -> Self {
3380 Self::new()
3381 }
3382}
3383
3384const EXTRA_DIM_SIZE_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3386 "HDF5_extraDimSizeN",
3387 "HDF5_extraDimSizeX",
3388 "HDF5_extraDimSizeY",
3389 "HDF5_extraDimSize3",
3390 "HDF5_extraDimSize4",
3391 "HDF5_extraDimSize5",
3392 "HDF5_extraDimSize6",
3393 "HDF5_extraDimSize7",
3394 "HDF5_extraDimSize8",
3395 "HDF5_extraDimSize9",
3396];
3397
3398const EXTRA_DIM_NAME_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3400 "HDF5_extraDimNameN",
3401 "HDF5_extraDimNameX",
3402 "HDF5_extraDimNameY",
3403 "HDF5_extraDimName3",
3404 "HDF5_extraDimName4",
3405 "HDF5_extraDimName5",
3406 "HDF5_extraDimName6",
3407 "HDF5_extraDimName7",
3408 "HDF5_extraDimName8",
3409 "HDF5_extraDimName9",
3410];
3411
3412impl NDPluginProcess for Hdf5FileProcessor {
3413 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3414 let was_swmr = self.ctrl.writer.is_swmr_active();
3415 let mut result = self.ctrl.process_array(array);
3416 let is_swmr = self.ctrl.writer.is_swmr_active();
3417
3418 if was_swmr != is_swmr {
3420 if let Some(idx) = self.hdf5_params.swmr_running {
3421 result
3422 .param_updates
3423 .push(ParamUpdate::int32(idx, if is_swmr { 1 } else { 0 }));
3424 }
3425 }
3426
3427 if is_swmr {
3429 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3430 result.param_updates.push(ParamUpdate::int32(
3431 idx,
3432 self.ctrl.writer.swmr_cb_counter as i32,
3433 ));
3434 }
3435 }
3436
3437 if self.ctrl.writer.store_performance {
3439 if let Some(idx) = self.hdf5_params.total_runtime {
3440 result
3441 .param_updates
3442 .push(ParamUpdate::float64(idx, self.ctrl.writer.total_runtime));
3443 }
3444 if let Some(idx) = self.hdf5_params.total_io_speed {
3445 let speed = if self.ctrl.writer.total_runtime > 0.0 {
3446 self.ctrl.writer.total_bytes as f64
3447 / self.ctrl.writer.total_runtime
3448 / 1_000_000.0
3449 } else {
3450 0.0
3451 };
3452 result.param_updates.push(ParamUpdate::float64(idx, speed));
3453 }
3454 }
3455
3456 result
3457 }
3458
3459 fn plugin_type(&self) -> &str {
3460 "NDFileHDF5"
3461 }
3462
3463 fn compression_aware(&self) -> bool {
3469 true
3470 }
3471
3472 fn does_array_callbacks(&self) -> bool {
3475 false
3476 }
3477
3478 fn register_params(
3479 &mut self,
3480 base: &mut asyn_rs::port::PortDriverBase,
3481 ) -> asyn_rs::error::AsynResult<()> {
3482 self.ctrl.register_params(base)?;
3483 register_hdf5_params(base)?;
3484 self.hdf5_params.compression_type = base.find_param("HDF5_compressionType");
3485 self.hdf5_params.z_compress_level = base.find_param("HDF5_zCompressLevel");
3486 self.hdf5_params.szip_num_pixels = base.find_param("HDF5_szipNumPixels");
3487 self.hdf5_params.nbit_precision = base.find_param("HDF5_nbitsPrecision");
3488 self.hdf5_params.nbit_offset = base.find_param("HDF5_nbitsOffset");
3489 self.hdf5_params.jpeg_quality = base.find_param("HDF5_jpegQuality");
3490 self.hdf5_params.blosc_shuffle_type = base.find_param("HDF5_bloscShuffleType");
3491 self.hdf5_params.blosc_compressor = base.find_param("HDF5_bloscCompressor");
3492 self.hdf5_params.blosc_compress_level = base.find_param("HDF5_bloscCompressLevel");
3493 self.hdf5_params.store_attributes = base.find_param("HDF5_storeAttributes");
3494 self.hdf5_params.store_performance = base.find_param("HDF5_storePerformance");
3495 self.hdf5_params.total_runtime = base.find_param("HDF5_totalRuntime");
3496 self.hdf5_params.total_io_speed = base.find_param("HDF5_totalIoSpeed");
3497 self.hdf5_params.swmr_mode = base.find_param("HDF5_SWMRMode");
3498 self.hdf5_params.swmr_flush_now = base.find_param("HDF5_SWMRFlushNow");
3499 self.hdf5_params.swmr_running = base.find_param("HDF5_SWMRRunning");
3500 self.hdf5_params.swmr_cb_counter = base.find_param("HDF5_SWMRCbCounter");
3501 self.hdf5_params.swmr_supported = base.find_param("HDF5_SWMRSupported");
3502 self.hdf5_params.flush_nth_frame = base.find_param("HDF5_flushNthFrame");
3503 self.hdf5_params.chunk_size_auto = base.find_param("HDF5_chunkSizeAuto");
3504 self.hdf5_params.n_row_chunks = base.find_param("HDF5_nRowChunks");
3505 self.hdf5_params.n_col_chunks = base.find_param("HDF5_nColChunks");
3506 self.hdf5_params.n_frames_chunks = base.find_param("HDF5_nFramesChunks");
3507 self.hdf5_params.ndattr_chunk = base.find_param("HDF5_NDAttributeChunk");
3508 self.hdf5_params.n_extra_dims = base.find_param("HDF5_nExtraDims");
3509 for i in 0..MAX_EXTRA_DIMS {
3510 self.hdf5_params.extra_dim_size[i] = base.find_param(EXTRA_DIM_SIZE_PARAMS[i]);
3511 self.hdf5_params.extra_dim_name[i] = base.find_param(EXTRA_DIM_NAME_PARAMS[i]);
3512 }
3513 self.hdf5_params.fill_value = base.find_param("HDF5_fillValue");
3514 self.hdf5_params.dim_att_datasets = base.find_param("HDF5_dimAttDatasets");
3515 self.hdf5_params.layout_filename = base.find_param("HDF5_layoutFilename");
3516 self.hdf5_params.layout_valid = base.find_param("HDF5_layoutValid");
3517 self.hdf5_params.layout_error_msg = base.find_param("HDF5_layoutErrorMsg");
3518
3519 if let Some(idx) = self.hdf5_params.swmr_supported {
3521 base.set_int32_param(idx, 0, 1)?;
3522 }
3523 Ok(())
3524 }
3525
3526 fn on_param_change(
3527 &mut self,
3528 reason: usize,
3529 params: &PluginParamSnapshot,
3530 ) -> ParamChangeResult {
3531 if Some(reason) == self.hdf5_params.compression_type {
3533 self.ctrl.writer.set_compression_type(params.value.as_i32());
3534 return ParamChangeResult::updates(vec![]);
3535 }
3536 if Some(reason) == self.hdf5_params.z_compress_level {
3537 self.ctrl
3538 .writer
3539 .set_z_compress_level(params.value.as_i32() as u32);
3540 return ParamChangeResult::updates(vec![]);
3541 }
3542 if Some(reason) == self.hdf5_params.szip_num_pixels {
3543 self.ctrl
3544 .writer
3545 .set_szip_num_pixels(params.value.as_i32() as u32);
3546 return ParamChangeResult::updates(vec![]);
3547 }
3548 if Some(reason) == self.hdf5_params.blosc_shuffle_type {
3549 self.ctrl
3550 .writer
3551 .set_blosc_shuffle_type(params.value.as_i32());
3552 return ParamChangeResult::updates(vec![]);
3553 }
3554 if Some(reason) == self.hdf5_params.blosc_compressor {
3555 self.ctrl.writer.set_blosc_compressor(params.value.as_i32());
3556 return ParamChangeResult::updates(vec![]);
3557 }
3558 if Some(reason) == self.hdf5_params.blosc_compress_level {
3559 self.ctrl
3560 .writer
3561 .set_blosc_compress_level(params.value.as_i32() as u32);
3562 return ParamChangeResult::updates(vec![]);
3563 }
3564 if Some(reason) == self.hdf5_params.nbit_precision {
3565 self.ctrl
3566 .writer
3567 .set_nbit_precision(params.value.as_i32() as u32);
3568 return ParamChangeResult::updates(vec![]);
3569 }
3570 if Some(reason) == self.hdf5_params.nbit_offset {
3571 self.ctrl
3572 .writer
3573 .set_nbit_offset(params.value.as_i32() as u32);
3574 return ParamChangeResult::updates(vec![]);
3575 }
3576 if Some(reason) == self.hdf5_params.jpeg_quality {
3577 self.ctrl
3578 .writer
3579 .set_jpeg_quality(params.value.as_i32() as u32);
3580 return ParamChangeResult::updates(vec![]);
3581 }
3582 if Some(reason) == self.hdf5_params.store_attributes {
3583 self.ctrl
3584 .writer
3585 .set_store_attributes(params.value.as_i32() != 0);
3586 return ParamChangeResult::updates(vec![]);
3587 }
3588 if Some(reason) == self.hdf5_params.store_performance {
3589 self.ctrl
3590 .writer
3591 .set_store_performance(params.value.as_i32() != 0);
3592 return ParamChangeResult::updates(vec![]);
3593 }
3594 if Some(reason) == self.hdf5_params.chunk_size_auto {
3596 self.ctrl
3597 .writer
3598 .set_chunk_size_auto(params.value.as_i32() != 0);
3599 return ParamChangeResult::updates(vec![]);
3600 }
3601 if Some(reason) == self.hdf5_params.n_row_chunks {
3602 self.ctrl
3603 .writer
3604 .set_n_row_chunks(params.value.as_i32().max(0) as usize);
3605 return ParamChangeResult::updates(vec![]);
3606 }
3607 if Some(reason) == self.hdf5_params.n_col_chunks {
3608 self.ctrl
3609 .writer
3610 .set_n_col_chunks(params.value.as_i32().max(0) as usize);
3611 return ParamChangeResult::updates(vec![]);
3612 }
3613 if Some(reason) == self.hdf5_params.n_frames_chunks {
3614 self.ctrl
3615 .writer
3616 .set_n_frames_chunks(params.value.as_i32().max(0) as usize);
3617 return ParamChangeResult::updates(vec![]);
3618 }
3619 if Some(reason) == self.hdf5_params.ndattr_chunk {
3620 self.ctrl
3622 .writer
3623 .set_ndattr_chunk(params.value.as_i32().max(0) as usize);
3624 return ParamChangeResult::updates(vec![]);
3625 }
3626 if Some(reason) == self.hdf5_params.n_extra_dims {
3628 self.ctrl
3629 .writer
3630 .set_n_extra_dims(params.value.as_i32().max(0) as usize);
3631 return ParamChangeResult::updates(vec![]);
3632 }
3633 for i in 0..MAX_EXTRA_DIMS {
3634 if Some(reason) == self.hdf5_params.extra_dim_size[i] {
3635 self.ctrl
3636 .writer
3637 .set_extra_dim_size(i, params.value.as_i32().max(1) as usize);
3638 return ParamChangeResult::updates(vec![]);
3639 }
3640 if Some(reason) == self.hdf5_params.extra_dim_name[i] {
3641 self.ctrl
3642 .writer
3643 .set_extra_dim_name(i, params.value.as_string().unwrap_or(""));
3644 return ParamChangeResult::updates(vec![]);
3645 }
3646 }
3647 if Some(reason) == self.hdf5_params.fill_value {
3648 self.ctrl.writer.set_fill_value(params.value.as_f64());
3649 return ParamChangeResult::updates(vec![]);
3650 }
3651 if Some(reason) == self.hdf5_params.dim_att_datasets {
3652 self.ctrl
3653 .writer
3654 .set_dim_att_datasets(params.value.as_i32() != 0);
3655 return ParamChangeResult::updates(vec![]);
3656 }
3657 if Some(reason) == self.hdf5_params.layout_filename {
3659 let path = params.value.as_string().unwrap_or("").to_string();
3660 self.ctrl.writer.set_layout_filename(&path);
3661 let mut updates = vec![];
3662 if let Some(idx) = self.hdf5_params.layout_valid {
3663 updates.push(ParamUpdate::int32(
3664 idx,
3665 if self.ctrl.writer.layout_valid { 1 } else { 0 },
3666 ));
3667 }
3668 if let Some(idx) = self.hdf5_params.layout_error_msg {
3669 updates.push(ParamUpdate::Octet {
3670 reason: idx,
3671 addr: 0,
3672 value: self.ctrl.writer.layout_error.clone(),
3673 });
3674 }
3675 return ParamChangeResult::updates(updates);
3676 }
3677 if Some(reason) == self.hdf5_params.swmr_mode {
3679 self.ctrl.writer.set_swmr_mode(params.value.as_i32() != 0);
3680 return ParamChangeResult::updates(vec![]);
3681 }
3682 if Some(reason) == self.hdf5_params.swmr_flush_now {
3683 if params.value.as_i32() != 0 {
3684 self.ctrl.writer.flush_swmr();
3685 let mut updates = vec![];
3686 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3687 updates.push(ParamUpdate::int32(
3688 idx,
3689 self.ctrl.writer.swmr_cb_counter as i32,
3690 ));
3691 }
3692 return ParamChangeResult::updates(updates);
3693 }
3694 return ParamChangeResult::updates(vec![]);
3695 }
3696 if Some(reason) == self.hdf5_params.flush_nth_frame {
3697 self.ctrl
3698 .writer
3699 .set_flush_nth_frame(params.value.as_i32().max(0) as usize);
3700 return ParamChangeResult::updates(vec![]);
3701 }
3702 self.ctrl.on_param_change(reason, params)
3703 }
3704}
3705
3706#[cfg(test)]
3707mod tests {
3708 use super::*;
3709 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
3710 use rust_hdf5::format::messages::filter::FILTER_LZ4;
3711 use std::sync::atomic::{AtomicU32, Ordering};
3712
3713 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
3714
3715 fn temp_path(prefix: &str) -> PathBuf {
3716 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
3717 std::env::temp_dir().join(format!("adcore_test_{}_{}.h5", prefix, n))
3718 }
3719
3720 #[test]
3721 fn test_write_single_frame() {
3722 let path = temp_path("hdf5_single");
3723 let mut writer = Hdf5Writer::new();
3724
3725 let mut arr = NDArray::new(
3726 vec![NDDimension::new(4), NDDimension::new(4)],
3727 NDDataType::UInt8,
3728 );
3729 if let NDDataBuffer::U8(ref mut v) = arr.data {
3730 for i in 0..16 {
3731 v[i] = i as u8;
3732 }
3733 }
3734
3735 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3736 writer.write_file(&arr).unwrap();
3737 writer.close_file().unwrap();
3738
3739 let h5 = H5File::open(&path).unwrap();
3741 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3742 assert_eq!(ds.shape(), vec![1, 4, 4]);
3743 let data: Vec<u8> = ds.read_raw().unwrap();
3744 assert_eq!(data[0], 0);
3745 assert_eq!(data[15], 15);
3746 drop(h5);
3747
3748 let mut reader = Hdf5Writer::new();
3749 reader.current_path = Some(path.clone());
3750 let read_arr = reader.read_file().unwrap();
3751 assert_eq!(read_arr.dims.len(), 3);
3752 assert_eq!(read_arr.dims[2].size, 1); std::fs::remove_file(&path).ok();
3755 }
3756
3757 #[test]
3758 fn test_parse_fsync_on_close_env() {
3759 assert!(Hdf5Writer::parse_fsync_on_close_env(None));
3761 for v in ["0", "false", "no", "off", "FALSE", "Off", " no "] {
3763 assert!(
3764 !Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3765 "{v:?} should disable fsync-on-close"
3766 );
3767 }
3768 for v in ["", "1", "true", "yes", "on", "durable"] {
3770 assert!(
3771 Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3772 "{v:?} should keep fsync-on-close"
3773 );
3774 }
3775 }
3776
3777 #[test]
3778 fn test_no_fsync_close_writes_valid_file() {
3779 let path = temp_path("hdf5_no_fsync");
3783 let mut writer = Hdf5Writer::new();
3784 writer.fsync_on_close = false;
3785
3786 let mut arr = NDArray::new(
3787 vec![NDDimension::new(4), NDDimension::new(4)],
3788 NDDataType::UInt8,
3789 );
3790 if let NDDataBuffer::U8(ref mut v) = arr.data {
3791 for i in 0..16 {
3792 v[i] = i as u8;
3793 }
3794 }
3795
3796 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3797 writer.write_file(&arr).unwrap();
3798 writer.close_file().unwrap();
3799
3800 let h5 = H5File::open(&path).unwrap();
3801 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3802 assert_eq!(ds.shape(), vec![1, 4, 4]);
3803 let data: Vec<u8> = ds.read_raw().unwrap();
3804 assert_eq!(data[0], 0);
3805 assert_eq!(data[15], 15);
3806 drop(h5);
3807
3808 std::fs::remove_file(&path).ok();
3809 }
3810
3811 #[test]
3812 fn test_write_multiple_frames() {
3813 let path = temp_path("hdf5_multi");
3814 let mut writer = Hdf5Writer::new();
3815
3816 let mut arr = NDArray::new(
3817 vec![NDDimension::new(4), NDDimension::new(4)],
3818 NDDataType::UInt8,
3819 );
3820 for f in 0..3u8 {
3822 if let NDDataBuffer::U8(ref mut v) = arr.data {
3823 for x in v.iter_mut() {
3824 *x = f;
3825 }
3826 }
3827 if f == 0 {
3828 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3829 }
3830 writer.write_file(&arr).unwrap();
3831 }
3832 writer.close_file().unwrap();
3833
3834 assert!(writer.supports_multiple_arrays());
3835 assert_eq!(writer.frame_count(), 3);
3836
3837 let data = std::fs::read(&path).unwrap();
3838 assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
3839
3840 let h5 = H5File::open(&path).unwrap();
3842 let names = h5.dataset_names();
3843 assert!(names.contains(&"entry/instrument/detector/data".to_string()));
3844 assert!(
3845 !names.contains(&"data_1".to_string()),
3846 "must not write per-frame datasets"
3847 );
3848 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3849 assert_eq!(
3850 ds.shape(),
3851 vec![3, 4, 4],
3852 "rank/shape must be [nframes,Y,X]"
3853 );
3854 let raw: Vec<u8> = ds.read_raw().unwrap();
3855 assert_eq!(raw.len(), 3 * 4 * 4);
3856 assert_eq!(raw[0], 0);
3858 assert_eq!(raw[16], 1);
3859 assert_eq!(raw[32], 2);
3860
3861 std::fs::remove_file(&path).ok();
3862 }
3863
3864 #[test]
3865 fn test_sub_frame_chunking() {
3866 let path = temp_path("hdf5_subchunk");
3870 let mut writer = Hdf5Writer::new();
3871 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3876 vec![NDDimension::new(8), NDDimension::new(8)],
3877 NDDataType::UInt16,
3878 );
3879 for f in 0..3u16 {
3880 if let NDDataBuffer::U16(ref mut v) = arr.data {
3881 for (i, x) in v.iter_mut().enumerate() {
3882 *x = f * 1000 + i as u16;
3883 }
3884 }
3885 if f == 0 {
3886 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3887 }
3888 writer.write_file(&arr).unwrap();
3889 }
3890 writer.close_file().unwrap();
3891
3892 let h5 = H5File::open(&path).unwrap();
3893 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3894 assert_eq!(ds.shape(), vec![3, 8, 8], "shape must not be chunk-padded");
3895 assert_eq!(
3896 ds.chunk_dims(),
3897 Some(vec![1, 4, 4]),
3898 "chunk grid must be the sub-frame tile size"
3899 );
3900 let raw: Vec<u16> = ds.read_raw().unwrap();
3901 assert_eq!(raw.len(), 3 * 64);
3902 for f in 0..3u16 {
3903 for i in 0..64usize {
3904 assert_eq!(
3905 raw[f as usize * 64 + i],
3906 f * 1000 + i as u16,
3907 "frame {} elem {}",
3908 f,
3909 i
3910 );
3911 }
3912 }
3913
3914 std::fs::remove_file(&path).ok();
3915 }
3916
3917 #[test]
3918 fn test_sub_frame_chunking_with_compression() {
3919 let path = temp_path("hdf5_subchunk_zlib");
3922 let mut writer = Hdf5Writer::new();
3923 writer.set_chunk_size_auto(false);
3924 writer.set_n_row_chunks(4);
3925 writer.set_n_col_chunks(4);
3926 writer.set_compression_type(COMPRESS_ZLIB);
3927
3928 let mut arr = NDArray::new(
3929 vec![NDDimension::new(8), NDDimension::new(8)],
3930 NDDataType::UInt16,
3931 );
3932 for f in 0..2u16 {
3933 if let NDDataBuffer::U16(ref mut v) = arr.data {
3934 for (i, x) in v.iter_mut().enumerate() {
3935 *x = f * 100 + i as u16;
3936 }
3937 }
3938 if f == 0 {
3939 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3940 }
3941 writer.write_file(&arr).unwrap();
3942 }
3943 writer.close_file().unwrap();
3944
3945 let h5 = H5File::open(&path).unwrap();
3946 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3947 assert_eq!(ds.shape(), vec![2, 8, 8]);
3948 assert_eq!(ds.chunk_dims(), Some(vec![1, 4, 4]));
3949 let raw: Vec<u16> = ds.read_raw().unwrap();
3950 for f in 0..2u16 {
3951 for i in 0..64usize {
3952 assert_eq!(raw[f as usize * 64 + i], f * 100 + i as u16);
3953 }
3954 }
3955
3956 std::fs::remove_file(&path).ok();
3957 }
3958
3959 #[test]
3960 fn test_non_dividing_chunk_is_honored_and_extent_trimmed() {
3961 let path = temp_path("hdf5_subchunk_nd");
3965 let mut writer = Hdf5Writer::new();
3966 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(3); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3971 vec![NDDimension::new(8), NDDimension::new(8)],
3972 NDDataType::UInt16,
3973 );
3974 if let NDDataBuffer::U16(ref mut v) = arr.data {
3975 for (i, x) in v.iter_mut().enumerate() {
3976 *x = i as u16;
3977 }
3978 }
3979 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3980 writer.write_file(&arr).unwrap();
3981 writer.write_file(&arr).unwrap();
3982 writer.close_file().unwrap();
3983
3984 let h5 = H5File::open(&path).unwrap();
3985 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3986 assert_eq!(ds.shape(), vec![2, 8, 8], "extent trimmed, not padded");
3987 assert_eq!(ds.chunk_dims(), Some(vec![1, 3, 4]));
3988 let raw: Vec<u16> = ds.read_raw().unwrap();
3989 assert_eq!(raw.len(), 2 * 64);
3990 for i in 0..64usize {
3991 assert_eq!(raw[i], i as u16);
3992 assert_eq!(raw[64 + i], i as u16);
3993 }
3994
3995 std::fs::remove_file(&path).ok();
3996 }
3997
3998 #[test]
3999 fn test_n_frames_chunks_band() {
4000 let path = temp_path("hdf5_framechunks");
4003 let mut writer = Hdf5Writer::new();
4004 writer.set_chunk_size_auto(false);
4005 writer.set_n_frames_chunks(2); let mut arr = NDArray::new(
4008 vec![NDDimension::new(4), NDDimension::new(4)],
4009 NDDataType::UInt16,
4010 );
4011 for f in 0..5u16 {
4013 if let NDDataBuffer::U16(ref mut v) = arr.data {
4014 for (i, x) in v.iter_mut().enumerate() {
4015 *x = f * 1000 + i as u16;
4016 }
4017 }
4018 if f == 0 {
4019 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4020 }
4021 writer.write_file(&arr).unwrap();
4022 }
4023 writer.close_file().unwrap();
4024
4025 let h5 = H5File::open(&path).unwrap();
4026 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4027 assert_eq!(ds.shape(), vec![5, 4, 4], "exact frame count, no padding");
4028 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4029 let raw: Vec<u16> = ds.read_raw().unwrap();
4030 for f in 0..5u16 {
4031 for i in 0..16usize {
4032 assert_eq!(raw[f as usize * 16 + i], f * 1000 + i as u16);
4033 }
4034 }
4035
4036 std::fs::remove_file(&path).ok();
4037 }
4038
4039 #[test]
4040 fn test_frames_chunks_with_sub_frame_tiles() {
4041 let path = temp_path("hdf5_full_chunk");
4045 let mut writer = Hdf5Writer::new();
4046 writer.set_chunk_size_auto(false);
4047 writer.set_n_frames_chunks(2); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
4052 vec![NDDimension::new(8), NDDimension::new(8)],
4053 NDDataType::UInt16,
4054 );
4055 for f in 0..3u16 {
4057 if let NDDataBuffer::U16(ref mut v) = arr.data {
4058 for (i, x) in v.iter_mut().enumerate() {
4059 *x = f * 1000 + i as u16;
4060 }
4061 }
4062 if f == 0 {
4063 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4064 }
4065 writer.write_file(&arr).unwrap();
4066 }
4067 writer.close_file().unwrap();
4068
4069 let h5 = H5File::open(&path).unwrap();
4070 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4071 assert_eq!(ds.shape(), vec![3, 8, 8], "exact frame count");
4072 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4073 let raw: Vec<u16> = ds.read_raw().unwrap();
4074 assert_eq!(raw.len(), 3 * 64);
4075 for f in 0..3u16 {
4076 for i in 0..64usize {
4077 assert_eq!(
4078 raw[f as usize * 64 + i],
4079 f * 1000 + i as u16,
4080 "frame {} elem {}",
4081 f,
4082 i
4083 );
4084 }
4085 }
4086
4087 std::fs::remove_file(&path).ok();
4088 }
4089
4090 #[test]
4091 fn test_attribute_datasets() {
4092 let path = temp_path("hdf5_attr_ds");
4093 let mut writer = Hdf5Writer::new();
4094
4095 let mk = |exposure: f64, count: i32| {
4096 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4097 arr.attributes.add(NDAttribute::new_static(
4098 "exposure",
4099 "",
4100 NDAttrSource::Driver,
4101 NDAttrValue::Float64(exposure),
4102 ));
4103 arr.attributes.add(NDAttribute::new_static(
4104 "count",
4105 "",
4106 NDAttrSource::Driver,
4107 NDAttrValue::Int32(count),
4108 ));
4109 arr
4110 };
4111
4112 let a0 = mk(0.5, 10);
4113 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4114 writer.write_file(&a0).unwrap();
4115 writer.write_file(&mk(0.75, 20)).unwrap();
4116 writer.write_file(&mk(1.25, 30)).unwrap();
4117 writer.close_file().unwrap();
4118
4119 let h5 = H5File::open(&path).unwrap();
4120 let exp = h5
4122 .dataset("entry/instrument/NDAttributes/exposure")
4123 .unwrap();
4124 assert_eq!(exp.shape(), vec![3]);
4125 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
4126 assert_eq!(exp_vals, vec![0.5, 0.75, 1.25]);
4127
4128 let cnt = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4129 assert_eq!(cnt.shape(), vec![3]);
4130 let cnt_vals: Vec<i32> = cnt.read_raw().unwrap();
4132 assert_eq!(cnt_vals, vec![10, 20, 30]);
4133
4134 std::fs::remove_file(&path).ok();
4135 }
4136
4137 #[test]
4138 fn test_attribute_dataset_chunk_matches_capture_target() {
4139 let path = temp_path("hdf5_attr_chunk_cap");
4146 let mut writer = Hdf5Writer::new();
4147 writer.set_num_capture(10);
4148
4149 let mk = |c: i32| {
4150 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4151 arr.attributes.add(NDAttribute::new_static(
4152 "count",
4153 "",
4154 NDAttrSource::Driver,
4155 NDAttrValue::Int32(c),
4156 ));
4157 arr
4158 };
4159
4160 let a0 = mk(1);
4161 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4162 writer.write_file(&a0).unwrap();
4163 writer.write_file(&mk(2)).unwrap();
4164 writer.write_file(&mk(3)).unwrap();
4165 writer.close_file().unwrap();
4166
4167 let h5 = H5File::open(&path).unwrap();
4168 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4169 assert_eq!(ds.shape(), vec![3]);
4170 assert_eq!(ds.chunk_dims(), Some(vec![10]));
4171 let vals: Vec<i32> = ds.read_raw().unwrap();
4172 assert_eq!(vals, vec![1, 2, 3]);
4173 std::fs::remove_file(&path).ok();
4174 }
4175
4176 #[test]
4177 fn test_attribute_dataset_chunk_single_mode_is_one() {
4178 let path = temp_path("hdf5_attr_chunk_single");
4182 let mut writer = Hdf5Writer::new();
4183 writer.set_num_capture(64);
4184
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 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4193 writer.write_file(&arr).unwrap();
4194 writer.close_file().unwrap();
4195
4196 let h5 = H5File::open(&path).unwrap();
4197 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4198 assert_eq!(ds.chunk_dims(), Some(vec![1]));
4199 std::fs::remove_file(&path).ok();
4200 }
4201
4202 #[test]
4203 fn test_string_attribute_dataset_is_rank1_fixed_string() {
4204 let path = temp_path("hdf5_attr_str");
4209 let mut writer = Hdf5Writer::new();
4210
4211 let mk = |mode_name: &str| {
4212 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4213 arr.attributes.add(NDAttribute::new_static(
4214 "ColorMode",
4215 "",
4216 NDAttrSource::Driver,
4217 NDAttrValue::String(mode_name.to_string()),
4218 ));
4219 arr
4220 };
4221
4222 let a0 = mk("Mono");
4223 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4224 writer.write_file(&a0).unwrap();
4225 writer.write_file(&mk("RGB1")).unwrap();
4226 writer.close_file().unwrap();
4227
4228 let h5 = H5File::open(&path).unwrap();
4229 let ds = h5
4232 .dataset("entry/instrument/detector/NDAttributes/ColorMode")
4233 .unwrap();
4234 assert_eq!(ds.shape(), vec![2]);
4236 assert_eq!(ds.element_size(), MAX_ATTRIBUTE_STRING_SIZE);
4237
4238 let frames: Vec<FixedStr256> = ds.read_raw().unwrap();
4239 assert_eq!(frames.len(), 2);
4240 let decode = |f: &FixedStr256| -> String {
4241 f.0.iter()
4242 .take_while(|&&b| b != 0)
4243 .map(|&b| b as char)
4244 .collect()
4245 };
4246 assert_eq!(decode(&frames[0]), "Mono");
4247 assert_eq!(decode(&frames[1]), "RGB1");
4248 std::fs::remove_file(&path).ok();
4249 }
4250
4251 #[test]
4252 fn test_ndattribute_dataset_routed_to_declared_group() {
4253 let path = temp_path("hdf5_attr_route");
4259 let mut writer = Hdf5Writer::new();
4260
4261 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4262 arr.attributes.add(NDAttribute::new_static(
4263 "ColorMode",
4264 "",
4265 NDAttrSource::Driver,
4266 NDAttrValue::String("Mono".to_string()),
4267 ));
4268 arr.attributes.add(NDAttribute::new_static(
4269 "count",
4270 "",
4271 NDAttrSource::Driver,
4272 NDAttrValue::Int32(7),
4273 ));
4274
4275 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4276 writer.write_file(&arr).unwrap();
4277 writer.close_file().unwrap();
4278
4279 let h5 = H5File::open(&path).unwrap();
4280 assert!(
4282 h5.dataset("entry/instrument/detector/NDAttributes/ColorMode")
4283 .is_ok()
4284 );
4285 assert!(h5.dataset("entry/instrument/NDAttributes/count").is_ok());
4287 assert!(
4289 h5.dataset("entry/instrument/NDAttributes/ColorMode")
4290 .is_err()
4291 );
4292 std::fs::remove_file(&path).ok();
4293 }
4294
4295 #[test]
4296 fn test_ndattribute_element_attr_open_close_values() {
4297 let dir = std::env::temp_dir();
4303 let layout = dir.join("adcore_layout_elem_attr.xml");
4304 std::fs::write(
4305 &layout,
4306 r#"<hdf5_layout>
4307 <group name="entry">
4308 <group name="instrument">
4309 <group name="detector">
4310 <attribute name="FirstColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4311 <attribute name="LastColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4312 <attribute name="DefaultColorMode" source="ndattribute" ndattribute="ColorMode"/>
4313 <dataset name="data" source="detector" det_default="true">
4314 <attribute name="GainAtOpen" source="ndattribute" ndattribute="Gain" when="OnFileOpen"/>
4315 <attribute name="GainAtClose" source="ndattribute" ndattribute="Gain" when="OnFileClose"/>
4316 </dataset>
4317 </group>
4318 <group name="NDAttributes" ndattr_default="true"/>
4319 </group>
4320 </group>
4321 </hdf5_layout>"#,
4322 )
4323 .unwrap();
4324
4325 let path = temp_path("hdf5_elem_attr");
4326 let mut writer = Hdf5Writer::new();
4327 assert!(
4328 writer.set_layout_filename(layout.to_str().unwrap()),
4329 "layout XML must parse: {}",
4330 writer.layout_error
4331 );
4332
4333 let mk = |mode: &str, gain: i32| {
4334 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4335 arr.attributes.add(NDAttribute::new_static(
4336 "ColorMode",
4337 "",
4338 NDAttrSource::Driver,
4339 NDAttrValue::String(mode.to_string()),
4340 ));
4341 arr.attributes.add(NDAttribute::new_static(
4342 "Gain",
4343 "",
4344 NDAttrSource::Driver,
4345 NDAttrValue::Int32(gain),
4346 ));
4347 arr
4348 };
4349
4350 let a0 = mk("Mono", 10);
4351 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4352 writer.write_file(&a0).unwrap();
4353 writer.write_file(&mk("RGB1", 20)).unwrap();
4354 writer.write_file(&mk("Bayer", 30)).unwrap();
4355 writer.close_file().unwrap();
4356
4357 let h5 = H5File::open(&path).unwrap();
4358
4359 let mut grp = h5.root_group();
4361 for seg in ["entry", "instrument", "detector"] {
4362 grp = grp.group(seg).unwrap();
4363 }
4364 assert_eq!(grp.attr_string("FirstColorMode").unwrap(), "Mono");
4366 assert_eq!(grp.attr_string("DefaultColorMode").unwrap(), "Mono");
4367 assert_eq!(grp.attr_string("LastColorMode").unwrap(), "Bayer");
4369
4370 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4372 assert_eq!(
4373 ds.attr("GainAtOpen")
4374 .unwrap()
4375 .read_numeric::<i32>()
4376 .unwrap(),
4377 10
4378 );
4379 assert_eq!(
4380 ds.attr("GainAtClose")
4381 .unwrap()
4382 .read_numeric::<i32>()
4383 .unwrap(),
4384 30
4385 );
4386
4387 std::fs::remove_file(&path).ok();
4388 std::fs::remove_file(&layout).ok();
4389 }
4390
4391 #[test]
4392 fn test_ndattr_element_attr_on_lazily_created_dataset() {
4393 let dir = std::env::temp_dir();
4401 let layout = dir.join("adcore_layout_lazy_ds_attr.xml");
4402 std::fs::write(
4403 &layout,
4404 r#"<hdf5_layout>
4405 <group name="entry">
4406 <group name="instrument">
4407 <group name="detector">
4408 <dataset name="data" source="detector" det_default="true"/>
4409 <dataset name="GainTrace" source="ndattribute" ndattribute="Gain">
4410 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4411 <attribute name="ModeAtClose" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4412 </dataset>
4413 </group>
4414 <group name="NDAttributes" ndattr_default="true"/>
4415 </group>
4416 </group>
4417 </hdf5_layout>"#,
4418 )
4419 .unwrap();
4420
4421 let path = temp_path("hdf5_lazy_ds_attr");
4422 let mut writer = Hdf5Writer::new();
4423 assert!(
4424 writer.set_layout_filename(layout.to_str().unwrap()),
4425 "layout XML must parse: {}",
4426 writer.layout_error
4427 );
4428
4429 let mk = |mode: &str, gain: i32| {
4430 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4431 arr.attributes.add(NDAttribute::new_static(
4432 "ColorMode",
4433 "",
4434 NDAttrSource::Driver,
4435 NDAttrValue::String(mode.to_string()),
4436 ));
4437 arr.attributes.add(NDAttribute::new_static(
4438 "Gain",
4439 "",
4440 NDAttrSource::Driver,
4441 NDAttrValue::Int32(gain),
4442 ));
4443 arr
4444 };
4445
4446 let a0 = mk("Mono", 10);
4447 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4448 writer.write_file(&a0).unwrap();
4449 writer.write_file(&mk("RGB1", 20)).unwrap();
4450 writer.write_file(&mk("Bayer", 30)).unwrap();
4451 writer.close_file().unwrap();
4452
4453 let h5 = H5File::open(&path).unwrap();
4454 let ds = h5.dataset("entry/instrument/detector/GainTrace").unwrap();
4457 assert_eq!(
4458 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4459 "Mono"
4460 );
4461 assert_eq!(
4462 ds.attr("ModeAtClose").unwrap().read_string().unwrap(),
4463 "Bayer"
4464 );
4465 std::fs::remove_file(&path).ok();
4466 std::fs::remove_file(&layout).ok();
4467 }
4468
4469 #[test]
4470 fn test_swmr_ndattr_element_attr_on_streaming_dataset() {
4471 let dir = std::env::temp_dir();
4477 let layout = dir.join("adcore_layout_swmr_ds_attr.xml");
4478 std::fs::write(
4479 &layout,
4480 r#"<hdf5_layout>
4481 <group name="entry">
4482 <group name="instrument">
4483 <group name="detector">
4484 <dataset name="data" source="detector" det_default="true">
4485 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4486 </dataset>
4487 </group>
4488 <group name="NDAttributes" ndattr_default="true"/>
4489 </group>
4490 </group>
4491 </hdf5_layout>"#,
4492 )
4493 .unwrap();
4494
4495 let path = temp_path("hdf5_swmr_ds_attr");
4496 let mut writer = Hdf5Writer::new();
4497 writer.set_swmr_mode(true);
4498 assert!(
4499 writer.set_layout_filename(layout.to_str().unwrap()),
4500 "layout XML must parse: {}",
4501 writer.layout_error
4502 );
4503
4504 let mk = |mode: &str| {
4505 let mut arr = NDArray::new(
4506 vec![NDDimension::new(4), NDDimension::new(4)],
4507 NDDataType::UInt16,
4508 );
4509 arr.attributes.add(NDAttribute::new_static(
4510 "ColorMode",
4511 "",
4512 NDAttrSource::Driver,
4513 NDAttrValue::String(mode.to_string()),
4514 ));
4515 arr
4516 };
4517
4518 let a0 = mk("Mono");
4519 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4520 writer.write_file(&a0).unwrap();
4521 writer.write_file(&mk("RGB1")).unwrap();
4522 writer.close_file().unwrap();
4523
4524 let h5 = H5File::open(&path).unwrap();
4525 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4526 assert_eq!(
4527 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4528 "Mono"
4529 );
4530 std::fs::remove_file(&path).ok();
4531 std::fs::remove_file(&layout).ok();
4532 }
4533
4534 #[test]
4535 fn test_swmr_ndarray_default_attrs_on_streaming_dataset() {
4536 let path = temp_path("hdf5_swmr_dimattr_1d");
4546 let mut writer = Hdf5Writer::new();
4547 writer.set_swmr_mode(true);
4548 let mut d = NDDimension::new(8);
4549 d.offset = 3;
4550 d.binning = 2;
4551 d.reverse = true;
4552 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4553 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4554 writer.write_file(&arr).unwrap();
4555 writer.write_file(&arr).unwrap();
4556 writer.close_file().unwrap();
4557 let h5 = H5File::open(&path).unwrap();
4558 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4559 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4560 assert_eq!(geti("NDArrayNumDims"), 1);
4561 assert_eq!(geti("NDArrayDimOffset"), 3);
4562 assert_eq!(geti("NDArrayDimBinning"), 2);
4563 assert_eq!(geti("NDArrayDimReverse"), 1);
4564 std::fs::remove_file(&path).ok();
4565
4566 let path = temp_path("hdf5_swmr_dimattr_2d");
4569 let mut writer = Hdf5Writer::new();
4570 writer.set_swmr_mode(true);
4571 let mut d0 = NDDimension::new(4);
4572 d0.offset = 3;
4573 d0.binning = 2;
4574 d0.reverse = true;
4575 let mut d1 = NDDimension::new(8);
4576 d1.offset = 5;
4577 d1.binning = 4;
4578 d1.reverse = false;
4579 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4580 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4581 writer.write_file(&arr).unwrap();
4582 writer.write_file(&arr).unwrap();
4583 writer.close_file().unwrap();
4584 let h5 = H5File::open(&path).unwrap();
4585 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4586 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4587 assert_eq!(numdims, 2);
4588 let read_i32_arr = |n: &str| -> Vec<i32> {
4589 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4590 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4591 raw.chunks_exact(4)
4592 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4593 .collect()
4594 };
4595 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4596 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4597 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4598 std::fs::remove_file(&path).ok();
4599 }
4600
4601 #[test]
4602 fn test_ndattr_descriptor_attributes_written() {
4603 let path = temp_path("hdf5_attr_desc");
4607 let mut writer = Hdf5Writer::new();
4608
4609 let mk = |v: f64| {
4610 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4611 arr.attributes.add(NDAttribute::new_static(
4612 "AcquireTime",
4613 "exposure time",
4614 NDAttrSource::EpicsPV("13SIM1:cam1:AcquireTime_RBV".to_string()),
4615 NDAttrValue::Float64(v),
4616 ));
4617 arr
4618 };
4619
4620 let a0 = mk(0.1);
4621 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4622 writer.write_file(&a0).unwrap();
4623 writer.write_file(&mk(0.2)).unwrap();
4624 writer.close_file().unwrap();
4625
4626 let h5 = H5File::open(&path).unwrap();
4627 let ds = h5
4628 .dataset("entry/instrument/NDAttributes/AcquireTime")
4629 .unwrap();
4630 let read = |n: &str| ds.attr(n).unwrap().read_string().unwrap();
4631 assert_eq!(read("NDAttrName"), "AcquireTime");
4632 assert_eq!(read("NDAttrDescription"), "exposure time");
4633 assert_eq!(read("NDAttrSourceType"), "NDAttrSourceEPICSPV");
4634 assert_eq!(read("NDAttrSource"), "13SIM1:cam1:AcquireTime_RBV");
4635 std::fs::remove_file(&path).ok();
4636 }
4637
4638 #[test]
4639 fn test_detector_dataset_ndarray_dim_attributes() {
4640 let path = temp_path("hdf5_dimattr_1d");
4648 let mut writer = Hdf5Writer::new();
4649 let mut d = NDDimension::new(8);
4650 d.offset = 3;
4651 d.binning = 2;
4652 d.reverse = true;
4653 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4654 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4655 writer.write_file(&arr).unwrap();
4656 writer.close_file().unwrap();
4657 let h5 = H5File::open(&path).unwrap();
4658 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4659 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4660 assert_eq!(geti("NDArrayNumDims"), 1);
4661 assert_eq!(geti("NDArrayDimOffset"), 3);
4662 assert_eq!(geti("NDArrayDimBinning"), 2);
4663 assert_eq!(geti("NDArrayDimReverse"), 1);
4664 std::fs::remove_file(&path).ok();
4665
4666 let path = temp_path("hdf5_dimattr_2d");
4670 let mut writer = Hdf5Writer::new();
4671 let mut d0 = NDDimension::new(4);
4672 d0.offset = 3;
4673 d0.binning = 2;
4674 d0.reverse = true;
4675 let mut d1 = NDDimension::new(8);
4676 d1.offset = 5;
4677 d1.binning = 4;
4678 d1.reverse = false;
4679 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4680 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4681 writer.write_file(&arr).unwrap();
4682 writer.close_file().unwrap();
4683 let h5 = H5File::open(&path).unwrap();
4684 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4685 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4686 assert_eq!(numdims, 2);
4687 let read_i32_arr = |n: &str| -> Vec<i32> {
4691 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4692 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4693 raw.chunks_exact(4)
4694 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4695 .collect()
4696 };
4697 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4698 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4699 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4700 std::fs::remove_file(&path).ok();
4701 }
4702
4703 #[test]
4704 fn test_fill_value_recorded_on_dataset() {
4705 let path = temp_path("hdf5_fill");
4710 let mut writer = Hdf5Writer::new();
4711 writer.set_fill_value(7.5);
4712
4713 let arr = NDArray::new(
4714 vec![NDDimension::new(4), NDDimension::new(4)],
4715 NDDataType::UInt16,
4716 );
4717 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4718 writer.write_file(&arr).unwrap();
4719 writer.close_file().unwrap();
4720
4721 let h5 = H5File::open(&path).unwrap();
4722 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4723 let fv: f64 = ds.attr("HDF5_fillValue").unwrap().read_numeric().unwrap();
4724 assert_eq!(fv, 7.5);
4725 std::fs::remove_file(&path).ok();
4726
4727 let path2 = temp_path("hdf5_fill_dcpl");
4730 {
4731 let f = H5File::create(&path2).unwrap();
4732 let _ = f
4733 .new_dataset::<i32>()
4734 .shape(&[8][..])
4735 .fill_value(42i32)
4736 .create("unwritten")
4737 .unwrap();
4738 }
4739 let h5b = H5File::open(&path2).unwrap();
4740 let vals: Vec<i32> = h5b.dataset("unwritten").unwrap().read_raw().unwrap();
4741 assert_eq!(vals, vec![42i32; 8]);
4742 std::fs::remove_file(&path2).ok();
4743 }
4744
4745 #[test]
4746 fn test_performance_dataset() {
4747 let path = temp_path("hdf5_perf");
4748 let mut writer = Hdf5Writer::new();
4749 writer.set_store_performance(true);
4750
4751 let arr = NDArray::new(
4752 vec![NDDimension::new(8), NDDimension::new(8)],
4753 NDDataType::UInt16,
4754 );
4755 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4756 writer.write_file(&arr).unwrap();
4757 writer.write_file(&arr).unwrap();
4758 writer.close_file().unwrap();
4759
4760 let h5 = H5File::open(&path).unwrap();
4761 let ts = h5
4762 .dataset("entry/instrument/performance/timestamp")
4763 .unwrap();
4764 assert_eq!(ts.shape(), vec![2, 5]);
4765 let vals: Vec<f64> = ts.read_raw().unwrap();
4766 assert_eq!(vals.len(), 10);
4767
4768 std::fs::remove_file(&path).ok();
4769 }
4770
4771 #[test]
4772 fn test_performance_dataset_chunk_matches_capture_target() {
4773 let path = temp_path("hdf5_perf_chunk");
4779 let mut writer = Hdf5Writer::new();
4780 writer.set_store_performance(true);
4781 writer.set_num_capture(8);
4782
4783 let arr = NDArray::new(
4784 vec![NDDimension::new(8), NDDimension::new(8)],
4785 NDDataType::UInt16,
4786 );
4787 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4788 writer.write_file(&arr).unwrap();
4789 writer.write_file(&arr).unwrap();
4790 writer.close_file().unwrap();
4791
4792 let h5 = H5File::open(&path).unwrap();
4793 let ts = h5
4794 .dataset("entry/instrument/performance/timestamp")
4795 .unwrap();
4796 assert_eq!(ts.shape(), vec![2, 5]);
4797 assert_eq!(ts.chunk_dims(), Some(vec![8, 5]));
4798 let vals: Vec<f64> = ts.read_raw().unwrap();
4799 assert_eq!(vals.len(), 10);
4800
4801 std::fs::remove_file(&path).ok();
4802 }
4803
4804 #[test]
4805 fn test_roundtrip_all_types() {
4806 macro_rules! roundtrip {
4807 ($name:expr, $dt:expr, $variant:ident, $ty:ty, $vals:expr) => {{
4808 let path = temp_path($name);
4809 let mut writer = Hdf5Writer::new();
4810 let mut arr = NDArray::new(vec![NDDimension::new(4)], $dt);
4811 if let NDDataBuffer::$variant(ref mut v) = arr.data {
4812 let src: Vec<$ty> = $vals;
4813 v.copy_from_slice(&src);
4814 }
4815 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4816 writer.write_file(&arr).unwrap();
4817 writer.close_file().unwrap();
4818
4819 let mut reader = Hdf5Writer::new();
4820 reader.current_path = Some(path.clone());
4821 let r = reader.read_file().unwrap();
4822 assert_eq!(r.data.data_type(), $dt, "type for {}", $name);
4823 if let NDDataBuffer::$variant(ref v) = r.data {
4824 let src: Vec<$ty> = $vals;
4825 assert_eq!(v, &src, "values for {}", $name);
4826 } else {
4827 panic!("wrong buffer variant for {}", $name);
4828 }
4829 std::fs::remove_file(&path).ok();
4830 }};
4831 }
4832
4833 roundtrip!("rt_i8", NDDataType::Int8, I8, i8, vec![-1, 0, 1, 127]);
4834 roundtrip!("rt_u8", NDDataType::UInt8, U8, u8, vec![0, 1, 200, 255]);
4835 roundtrip!(
4836 "rt_i16",
4837 NDDataType::Int16,
4838 I16,
4839 i16,
4840 vec![-32768, -1, 1, 32767]
4841 );
4842 roundtrip!(
4843 "rt_u16",
4844 NDDataType::UInt16,
4845 U16,
4846 u16,
4847 vec![0, 1, 40000, 65535]
4848 );
4849 roundtrip!(
4850 "rt_i32",
4851 NDDataType::Int32,
4852 I32,
4853 i32,
4854 vec![i32::MIN, -1, 1, i32::MAX]
4855 );
4856 roundtrip!(
4857 "rt_u32",
4858 NDDataType::UInt32,
4859 U32,
4860 u32,
4861 vec![0, 1, 3_000_000_000, u32::MAX]
4862 );
4863 roundtrip!(
4864 "rt_i64",
4865 NDDataType::Int64,
4866 I64,
4867 i64,
4868 vec![i64::MIN, -1, 1, i64::MAX]
4869 );
4870 roundtrip!(
4871 "rt_u64",
4872 NDDataType::UInt64,
4873 U64,
4874 u64,
4875 vec![0, 1, 9_000_000_000, u64::MAX]
4876 );
4877 roundtrip!(
4878 "rt_f32",
4879 NDDataType::Float32,
4880 F32,
4881 f32,
4882 vec![-1.5, 0.0, 2.25, 3.75]
4883 );
4884 roundtrip!(
4885 "rt_f64",
4886 NDDataType::Float64,
4887 F64,
4888 f64,
4889 vec![-1.5, 0.0, 2.25, 3.75]
4890 );
4891 }
4892
4893 #[test]
4894 fn test_deflate_compressed_write() {
4895 let path = temp_path("hdf5_deflate");
4896 let mut writer = Hdf5Writer::new();
4897 writer.set_compression_type(COMPRESS_ZLIB);
4898 writer.set_z_compress_level(6);
4899
4900 let mut arr = NDArray::new(
4901 vec![NDDimension::new(64), NDDimension::new(64)],
4902 NDDataType::UInt16,
4903 );
4904 if let NDDataBuffer::U16(ref mut v) = arr.data {
4905 for i in 0..v.len() {
4906 v[i] = (i % 256) as u16;
4907 }
4908 }
4909
4910 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4911 writer.write_file(&arr).unwrap();
4912 writer.close_file().unwrap();
4913
4914 let raw_path = temp_path("hdf5_deflate_raw");
4924 {
4925 let mut raw_writer = Hdf5Writer::new();
4926 raw_writer.set_compression_type(COMPRESS_NONE);
4927 raw_writer
4928 .open_file(&raw_path, NDFileMode::Single, &arr)
4929 .unwrap();
4930 raw_writer.write_file(&arr).unwrap();
4931 raw_writer.close_file().unwrap();
4932 }
4933 let compressed_size = std::fs::metadata(&path).unwrap().len();
4934 let raw_size = std::fs::metadata(&raw_path).unwrap().len();
4935 assert!(
4936 compressed_size + 4096 < raw_size,
4937 "deflate must shrink the data chunk: compressed={compressed_size} raw={raw_size}"
4938 );
4939
4940 let h5file = H5File::open(&path).unwrap();
4941 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4942 let data: Vec<u16> = ds.read_raw().unwrap();
4943 assert_eq!(data.len(), 64 * 64);
4944 assert_eq!(data[0], 0);
4945 assert_eq!(data[255], 255);
4946 assert_eq!(data[256], 0);
4947
4948 std::fs::remove_file(&path).ok();
4949 }
4950
4951 #[test]
4952 fn test_lz4_compressed_write() {
4953 let path = temp_path("hdf5_lz4");
4954 let mut writer = Hdf5Writer::new();
4955 writer.set_compression_type(COMPRESS_LZ4);
4956
4957 let mut arr = NDArray::new(
4958 vec![NDDimension::new(32), NDDimension::new(32)],
4959 NDDataType::UInt8,
4960 );
4961 if let NDDataBuffer::U8(ref mut v) = arr.data {
4962 for i in 0..v.len() {
4963 v[i] = (i % 4) as u8;
4964 }
4965 }
4966
4967 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4968 writer.write_file(&arr).unwrap();
4969 writer.close_file().unwrap();
4970
4971 let h5file = H5File::open(&path).unwrap();
4972 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4973 let data: Vec<u8> = ds.read_raw().unwrap();
4974 assert_eq!(data.len(), 32 * 32);
4975 assert_eq!(data[0], 0);
4976 assert_eq!(data[3], 3);
4977
4978 std::fs::remove_file(&path).ok();
4979 }
4980
4981 #[test]
4982 fn test_bitshuffle_compressed_write() {
4983 let path = temp_path("hdf5_bshuf");
4984 let mut writer = Hdf5Writer::new();
4985 writer.set_compression_type(COMPRESS_BSHUF);
4986
4987 let mut arr = NDArray::new(
4988 vec![NDDimension::new(64), NDDimension::new(64)],
4989 NDDataType::UInt16,
4990 );
4991 if let NDDataBuffer::U16(ref mut v) = arr.data {
4992 for i in 0..v.len() {
4993 v[i] = (i % 8) as u16;
4994 }
4995 }
4996
4997 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4998 writer.write_file(&arr).unwrap();
4999 writer.close_file().unwrap();
5000
5001 let h5file = H5File::open(&path).unwrap();
5002 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5003 let data: Vec<u16> = ds.read_raw().unwrap();
5004 assert_eq!(data.len(), 64 * 64);
5005 assert_eq!(data[0], 0);
5006 assert_eq!(data[9], 1);
5007
5008 std::fs::remove_file(&path).ok();
5009 }
5010
5011 #[test]
5012 fn test_szip_uses_nearest_neighbor_mask() {
5013 let mut writer = Hdf5Writer::new();
5016 writer.set_compression_type(COMPRESS_SZIP);
5017 let pipeline = writer.build_pipeline(1).expect("szip pipeline");
5018 assert_eq!(pipeline.filters[0].cd_values[0], SZIP_NN_OPTION_MASK);
5019
5020 let path = temp_path("hdf5_szip_nn");
5021 let mut arr = NDArray::new(
5022 vec![NDDimension::new(64), NDDimension::new(64)],
5023 NDDataType::UInt8,
5024 );
5025 if let NDDataBuffer::U8(ref mut v) = arr.data {
5026 for (i, e) in v.iter_mut().enumerate() {
5027 *e = (i % 13) as u8;
5028 }
5029 }
5030 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5031 writer.write_file(&arr).unwrap();
5032 writer.close_file().unwrap();
5033
5034 let h5file = H5File::open(&path).unwrap();
5035 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5036 let data: Vec<u8> = ds.read_raw().unwrap();
5037 assert_eq!(data.len(), 64 * 64);
5038 assert_eq!(data[20], (20 % 13) as u8);
5039 std::fs::remove_file(&path).ok();
5040 }
5041
5042 #[test]
5043 fn test_blosc_default_shuffle_is_byte_shuffle() {
5044 let mut writer = Hdf5Writer::new();
5047 writer.set_compression_type(COMPRESS_BLOSC);
5048 let pipeline = writer.build_pipeline(2).expect("blosc pipeline");
5049 assert_eq!(pipeline.filters[0].cd_values[5], 1);
5050 }
5051
5052 #[test]
5053 fn test_nbit_packs_to_reduced_precision_datatype() {
5054 let mut writer = Hdf5Writer::new();
5064 writer.set_compression_type(COMPRESS_NBIT);
5065 writer.set_nbit_precision(10);
5066 writer.set_nbit_offset(0);
5067 assert!(
5068 writer.build_pipeline(2).is_none(),
5069 "N-bit packing is applied via a datatype override, not build_pipeline"
5070 );
5071
5072 let path = temp_path("hdf5_nbit_packed");
5073 let mut arr = NDArray::new(
5074 vec![NDDimension::new(8), NDDimension::new(8)],
5075 NDDataType::UInt16,
5076 );
5077 if let NDDataBuffer::U16(ref mut v) = arr.data {
5078 for (i, x) in v.iter_mut().enumerate() {
5079 *x = (i as u16 * 7) & 0x3FF; }
5081 v[0] = 0xFFFF; }
5083 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5084 writer.write_file(&arr).unwrap();
5085 writer.close_file().unwrap();
5086
5087 let h5file = H5File::open(&path).unwrap();
5088 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5089
5090 match ds.datatype().unwrap() {
5093 DatatypeMessage::FixedPoint {
5094 size,
5095 bit_precision,
5096 bit_offset,
5097 signed,
5098 ..
5099 } => {
5100 assert_eq!(size, 2, "byte footprint unchanged");
5101 assert_eq!(bit_precision, 10, "precision narrowed to 10 bits");
5102 assert_eq!(bit_offset, 0);
5103 assert!(!signed, "u16 is unsigned");
5104 }
5105 other => panic!("expected reduced-precision FixedPoint, got {other:?}"),
5106 }
5107
5108 let data: Vec<u16> = ds.read_raw().unwrap();
5109 assert_eq!(data.len(), 8 * 8);
5110 assert_eq!(data[0], 0x3FF, "0xFFFF must pack to the low 10 bits");
5112 for i in 1..data.len() {
5113 assert_eq!(
5114 data[i],
5115 (i as u16 * 7) & 0x3FF,
5116 "in-range value {i} round-trips"
5117 );
5118 }
5119 std::fs::remove_file(&path).ok();
5120 }
5121
5122 #[test]
5123 fn test_chunk_geometry_recorded() {
5124 let path = temp_path("hdf5_chunkgeom");
5127 let mut writer = Hdf5Writer::new();
5128 writer.set_chunk_size_auto(false);
5129 writer.set_n_row_chunks(4);
5130 writer.set_n_col_chunks(2);
5131 writer.set_n_frames_chunks(3);
5132
5133 let mut arr = NDArray::new(
5134 vec![NDDimension::new(8), NDDimension::new(8)],
5135 NDDataType::UInt16,
5136 );
5137 if let NDDataBuffer::U16(ref mut v) = arr.data {
5138 for i in 0..v.len() {
5139 v[i] = i as u16;
5140 }
5141 }
5142
5143 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5144 writer.write_file(&arr).unwrap();
5145 writer.write_file(&arr).unwrap();
5146 writer.close_file().unwrap();
5147
5148 let h5 = H5File::open(&path).unwrap();
5149 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5150 assert_eq!(ds.shape(), vec![2, 8, 8]);
5151 let data: Vec<u16> = ds.read_raw().unwrap();
5153 assert_eq!(data.len(), 2 * 64);
5154 for i in 0..64usize {
5155 assert_eq!(data[i], i as u16, "frame0 element {}", i);
5156 assert_eq!(data[64 + i], i as u16, "frame1 element {}", i);
5157 }
5158 assert_eq!(
5160 ds.attr("HDF5_nRowChunks")
5161 .unwrap()
5162 .read_numeric::<i32>()
5163 .unwrap(),
5164 4
5165 );
5166 assert_eq!(
5167 ds.attr("HDF5_nColChunks")
5168 .unwrap()
5169 .read_numeric::<i32>()
5170 .unwrap(),
5171 2
5172 );
5173 assert_eq!(
5174 ds.attr("HDF5_nFramesChunks")
5175 .unwrap()
5176 .read_numeric::<i32>()
5177 .unwrap(),
5178 3
5179 );
5180
5181 std::fs::remove_file(&path).ok();
5182 }
5183
5184 #[test]
5185 fn test_extra_dimensions_layout() {
5186 let path = temp_path("hdf5_extradims");
5195 let mut writer = Hdf5Writer::new();
5196 writer.set_n_extra_dims(2);
5197 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");
5201 writer.set_extra_dim_name(1, "scanX");
5202 writer.set_extra_dim_name(2, "scanY");
5203
5204 let mut arr = NDArray::new(
5205 vec![NDDimension::new(4), NDDimension::new(4)],
5206 NDDataType::UInt16,
5207 );
5208 for f in 0..24u16 {
5209 if let NDDataBuffer::U16(ref mut v) = arr.data {
5210 for x in v.iter_mut() {
5211 *x = f;
5212 }
5213 }
5214 if f == 0 {
5215 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5216 }
5217 writer.write_file(&arr).unwrap();
5218 }
5219 writer.close_file().unwrap();
5220
5221 let h5 = H5File::open(&path).unwrap();
5222 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5223 assert_eq!(ds.shape(), vec![4, 3, 2, 4, 4]);
5225 let data: Vec<u16> = ds.read_raw().unwrap();
5226 assert_eq!(data.len(), 24 * 16);
5227 for f in 0..24usize {
5228 for i in 0..16usize {
5229 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5230 }
5231 }
5232 assert_eq!(
5234 ds.attr("HDF5_nExtraDims")
5235 .unwrap()
5236 .read_numeric::<i32>()
5237 .unwrap(),
5238 2
5239 );
5240 assert_eq!(
5241 ds.attr("HDF5_extraDimSize0")
5242 .unwrap()
5243 .read_numeric::<i32>()
5244 .unwrap(),
5245 2
5246 );
5247 assert_eq!(
5248 ds.attr("HDF5_extraDimName0")
5249 .unwrap()
5250 .read_string()
5251 .unwrap(),
5252 "n"
5253 );
5254
5255 std::fs::remove_file(&path).ok();
5256 }
5257
5258 #[test]
5259 fn test_extra_dimensions_one_virtual() {
5260 let path = temp_path("hdf5_extradims_1");
5264 let mut writer = Hdf5Writer::new();
5265 writer.set_n_extra_dims(1);
5266 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5270 vec![NDDimension::new(4), NDDimension::new(4)],
5271 NDDataType::UInt16,
5272 );
5273 for f in 0..6u16 {
5274 if let NDDataBuffer::U16(ref mut v) = arr.data {
5275 for x in v.iter_mut() {
5276 *x = f;
5277 }
5278 }
5279 if f == 0 {
5280 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5281 }
5282 writer.write_file(&arr).unwrap();
5283 }
5284 writer.close_file().unwrap();
5285
5286 let h5 = H5File::open(&path).unwrap();
5287 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5288 assert_eq!(ds.shape(), vec![3, 2, 4, 4]);
5289 let data: Vec<u16> = ds.read_raw().unwrap();
5290 assert_eq!(data.len(), 6 * 16);
5291 for f in 0..6usize {
5292 for i in 0..16usize {
5293 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5294 }
5295 }
5296
5297 std::fs::remove_file(&path).ok();
5298 }
5299
5300 #[test]
5301 fn test_swmr_extra_dimensions_grid_layout() {
5302 let path = temp_path("hdf5_swmr_extradims");
5309 let mut writer = Hdf5Writer::new();
5310 writer.set_swmr_mode(true);
5311 writer.set_n_extra_dims(2);
5312 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(
5317 vec![NDDimension::new(4), NDDimension::new(4)],
5318 NDDataType::UInt16,
5319 );
5320 for f in 0..24u16 {
5321 if let NDDataBuffer::U16(ref mut v) = arr.data {
5322 for x in v.iter_mut() {
5323 *x = f;
5324 }
5325 }
5326 if f == 0 {
5327 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5328 }
5329 writer.write_file(&arr).unwrap();
5330 }
5331 writer.close_file().unwrap();
5332
5333 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5334 assert_eq!(
5335 reader
5336 .dataset_shape("entry/instrument/detector/data")
5337 .unwrap(),
5338 vec![4, 3, 2, 4, 4]
5339 );
5340 let data: Vec<u16> = reader
5341 .read_dataset("entry/instrument/detector/data")
5342 .unwrap();
5343 assert_eq!(data.len(), 24 * 16);
5344 for f in 0..24usize {
5345 for i in 0..16usize {
5346 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5347 }
5348 }
5349 std::fs::remove_file(&path).ok();
5350 }
5351
5352 #[test]
5353 fn test_swmr_extra_dimensions_grid_subtiled() {
5354 let path = temp_path("hdf5_swmr_extradims_tiled");
5360 let mut writer = Hdf5Writer::new();
5361 writer.set_swmr_mode(true);
5362 writer.set_n_extra_dims(1);
5363 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(
5368 vec![NDDimension::new(4), NDDimension::new(4)],
5369 NDDataType::UInt16,
5370 );
5371 for f in 0..6u16 {
5372 if let NDDataBuffer::U16(ref mut v) = arr.data {
5373 for (i, x) in v.iter_mut().enumerate() {
5376 *x = f * 100 + i as u16;
5377 }
5378 }
5379 if f == 0 {
5380 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5381 }
5382 writer.write_file(&arr).unwrap();
5383 }
5384 writer.close_file().unwrap();
5385
5386 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5387 assert_eq!(
5388 reader
5389 .dataset_shape("entry/instrument/detector/data")
5390 .unwrap(),
5391 vec![3, 2, 4, 4]
5392 );
5393 let data: Vec<u16> = reader
5394 .read_dataset("entry/instrument/detector/data")
5395 .unwrap();
5396 assert_eq!(data.len(), 6 * 16);
5397 for f in 0..6usize {
5398 for i in 0..16usize {
5399 assert_eq!(
5400 data[f * 16 + i],
5401 f as u16 * 100 + i as u16,
5402 "frame {} elem {}",
5403 f,
5404 i
5405 );
5406 }
5407 }
5408 std::fs::remove_file(&path).ok();
5409 }
5410
5411 #[test]
5412 fn test_swmr_extra_dimensions_grid_partial_fill() {
5413 let path = temp_path("hdf5_swmr_extradims_partial");
5417 let mut writer = Hdf5Writer::new();
5418 writer.set_swmr_mode(true);
5419 writer.set_n_extra_dims(1);
5420 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5424 vec![NDDimension::new(4), NDDimension::new(4)],
5425 NDDataType::UInt16,
5426 );
5427 for f in 0..4u16 {
5428 if let NDDataBuffer::U16(ref mut v) = arr.data {
5429 for x in v.iter_mut() {
5430 *x = f + 1; }
5432 }
5433 if f == 0 {
5434 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5435 }
5436 writer.write_file(&arr).unwrap();
5437 }
5438 writer.close_file().unwrap();
5439
5440 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5441 assert_eq!(
5442 reader
5443 .dataset_shape("entry/instrument/detector/data")
5444 .unwrap(),
5445 vec![3, 2, 4, 4]
5446 );
5447 let data: Vec<u16> = reader
5448 .read_dataset("entry/instrument/detector/data")
5449 .unwrap();
5450 assert_eq!(data.len(), 6 * 16);
5451 for f in 0..6usize {
5452 let expected = if f < 4 { f as u16 + 1 } else { 0 };
5453 for i in 0..16usize {
5454 assert_eq!(data[f * 16 + i], expected, "frame {} elem {}", f, i);
5455 }
5456 }
5457 std::fs::remove_file(&path).ok();
5458 }
5459
5460 #[test]
5461 fn test_swmr_streaming() {
5462 let path = temp_path("hdf5_swmr");
5463 let mut writer = Hdf5Writer::new();
5464 writer.set_swmr_mode(true);
5465 writer.set_flush_nth_frame(2);
5466
5467 let arr = NDArray::new(
5468 vec![NDDimension::new(8), NDDimension::new(8)],
5469 NDDataType::Float32,
5470 );
5471
5472 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5473 writer.write_file(&arr).unwrap();
5474 writer.write_file(&arr).unwrap(); writer.write_file(&arr).unwrap();
5476 writer.close_file().unwrap();
5477
5478 assert_eq!(writer.frame_count(), 3);
5479
5480 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5482 let shape = reader
5483 .dataset_shape("entry/instrument/detector/data")
5484 .unwrap();
5485 assert_eq!(shape[0], 3); assert_eq!(shape[1], 8);
5487 assert_eq!(shape[2], 8);
5488
5489 let data: Vec<f32> = reader
5490 .read_dataset("entry/instrument/detector/data")
5491 .unwrap();
5492 assert_eq!(data.len(), 3 * 8 * 8);
5493
5494 std::fs::remove_file(&path).ok();
5495 }
5496
5497 #[test]
5498 fn test_swmr_compression_is_applied() {
5499 let path = temp_path("hdf5_swmr_comp");
5503 let mut writer = Hdf5Writer::new();
5504 writer.set_swmr_mode(true);
5505 writer.set_compression_type(COMPRESS_ZLIB);
5506
5507 let arr = NDArray::new(
5508 vec![NDDimension::new(8), NDDimension::new(8)],
5509 NDDataType::UInt16,
5510 );
5511 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5512 assert!(
5513 !writer.swmr_compression_dropped(),
5514 "SWMR+ZLIB must apply compression, not drop it"
5515 );
5516 writer.write_file(&arr).unwrap();
5517 writer.write_file(&arr).unwrap();
5518 writer.close_file().unwrap();
5519
5520 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5522 let shape = reader
5523 .dataset_shape("entry/instrument/detector/data")
5524 .unwrap();
5525 assert_eq!(shape, vec![2, 8, 8]);
5526 let data: Vec<u16> = reader
5527 .read_dataset("entry/instrument/detector/data")
5528 .unwrap();
5529 assert_eq!(data.len(), 2 * 8 * 8);
5530
5531 std::fs::remove_file(&path).ok();
5532 }
5533
5534 #[test]
5535 fn test_layout_xml_param() {
5536 let mut writer = Hdf5Writer::new();
5538 let dir = std::env::temp_dir();
5539 let good = dir.join("adcore_layout_good.xml");
5540 std::fs::write(
5541 &good,
5542 r#"<hdf5_layout><group name="entry"><dataset name="data" source="detector" det_default="true"/></group></hdf5_layout>"#,
5543 )
5544 .unwrap();
5545 assert!(writer.set_layout_filename(good.to_str().unwrap()));
5546 assert!(writer.layout_valid);
5547 assert!(writer.layout_error.is_empty());
5548
5549 let bad = dir.join("adcore_layout_bad.xml");
5550 std::fs::write(&bad, r#"<not_a_layout/>"#).unwrap();
5551 assert!(!writer.set_layout_filename(bad.to_str().unwrap()));
5552 assert!(!writer.layout_valid);
5553 assert!(!writer.layout_error.is_empty());
5554
5555 std::fs::remove_file(&good).ok();
5556 std::fs::remove_file(&bad).ok();
5557 }
5558
5559 #[test]
5560 fn test_layout_xml_places_dataset_in_nested_tree() {
5561 let dir = std::env::temp_dir();
5567 let layout = dir.join("adcore_layout_nested.xml");
5568 std::fs::write(
5569 &layout,
5570 r#"<hdf5_layout>
5571 <group name="entry">
5572 <group name="instrument">
5573 <group name="detector">
5574 <dataset name="data" source="detector" det_default="true">
5575 <attribute name="signal" source="constant" value="1" type="int"/>
5576 </dataset>
5577 </group>
5578 <group name="NDAttributes" ndattr_default="true"/>
5579 <group name="performance">
5580 <dataset name="timestamp"/>
5581 </group>
5582 </group>
5583 </group>
5584 </hdf5_layout>"#,
5585 )
5586 .unwrap();
5587
5588 let path = temp_path("hdf5_layout_nested");
5589 let mut writer = Hdf5Writer::new();
5590 writer.set_store_performance(true);
5591 assert!(
5592 writer.set_layout_filename(layout.to_str().unwrap()),
5593 "layout XML must parse: {}",
5594 writer.layout_error
5595 );
5596
5597 let mk = |fill: f64| {
5598 let mut arr = NDArray::new(
5599 vec![NDDimension::new(4), NDDimension::new(4)],
5600 NDDataType::UInt16,
5601 );
5602 arr.attributes.add(NDAttribute::new_static(
5603 "exposure",
5604 "",
5605 NDAttrSource::Driver,
5606 NDAttrValue::Float64(fill),
5607 ));
5608 arr
5609 };
5610
5611 let a0 = mk(0.5);
5612 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
5613 writer.write_file(&a0).unwrap();
5614 writer.write_file(&mk(0.75)).unwrap();
5615 writer.close_file().unwrap();
5616
5617 let h5 = H5File::open(&path).unwrap();
5618 let names = h5.dataset_names();
5619 assert!(
5621 names.contains(&"entry/instrument/detector/data".to_string()),
5622 "image dataset must be at the nested layout path; got {:?}",
5623 names
5624 );
5625 assert!(
5626 !names.contains(&"data".to_string()),
5627 "must not also write a flat-root `data` dataset"
5628 );
5629 let img = h5.dataset("entry/instrument/detector/data").unwrap();
5630 assert_eq!(img.shape(), vec![2, 4, 4]);
5631 assert_eq!(
5633 img.attr("signal").unwrap().read_numeric::<i64>().unwrap(),
5634 1
5635 );
5636 assert!(
5638 names.contains(&"entry/instrument/NDAttributes/exposure".to_string()),
5639 "NDAttribute dataset must be under the layout ndattr group; got {:?}",
5640 names
5641 );
5642 assert!(
5644 names.contains(&"entry/instrument/performance/timestamp".to_string()),
5645 "performance dataset must be under the layout group; got {:?}",
5646 names
5647 );
5648
5649 drop(h5);
5651 let mut reader = Hdf5Writer::new();
5652 assert!(reader.set_layout_filename(layout.to_str().unwrap()));
5653 reader.current_path = Some(path.clone());
5654 let read_arr = reader.read_file().unwrap();
5655 assert_eq!(read_arr.dims.len(), 3);
5656
5657 std::fs::remove_file(&path).ok();
5658 std::fs::remove_file(&layout).ok();
5659 }
5660
5661 #[test]
5669 fn test_detector_data_destination_routes_by_attribute() {
5670 let dir = std::env::temp_dir();
5671 let layout = dir.join("adcore_layout_multidet.xml");
5672 std::fs::write(
5673 &layout,
5674 r#"<hdf5_layout>
5675 <global name="detector_data_destination" ndattribute="dest"/>
5676 <group name="entry">
5677 <dataset name="data1" source="detector" det_default="true"/>
5678 <dataset name="data2" source="detector"/>
5679 </group>
5680 </hdf5_layout>"#,
5681 )
5682 .unwrap();
5683
5684 let path = temp_path("hdf5_multidet_route");
5685 let mut writer = Hdf5Writer::new();
5686 writer.store_attributes = false;
5688 assert!(
5689 writer.set_layout_filename(layout.to_str().unwrap()),
5690 "layout XML must parse: {}",
5691 writer.layout_error
5692 );
5693
5694 let mk = |val: u16, dest: Option<&str>| {
5697 let mut arr = NDArray::new(
5698 vec![NDDimension::new(2), NDDimension::new(2)],
5699 NDDataType::UInt16,
5700 );
5701 if let NDDataBuffer::U16(ref mut v) = arr.data {
5702 for p in v.iter_mut() {
5703 *p = val;
5704 }
5705 }
5706 if let Some(d) = dest {
5707 arr.attributes.add(NDAttribute::new_static(
5708 "dest",
5709 "",
5710 NDAttrSource::Driver,
5711 NDAttrValue::String(d.to_string()),
5712 ));
5713 }
5714 arr
5715 };
5716
5717 let f0 = mk(10, None);
5723 writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
5724 writer.write_file(&f0).unwrap();
5725 writer.write_file(&mk(11, Some("/entry/data2"))).unwrap();
5726 writer.write_file(&mk(12, Some("/entry/data2"))).unwrap();
5727 writer.write_file(&mk(13, Some("/nonexistent"))).unwrap();
5728 writer.write_file(&mk(14, Some("/entry/data1"))).unwrap();
5729 writer.close_file().unwrap();
5730
5731 let h5 = H5File::open(&path).unwrap();
5732 let names = h5.dataset_names();
5733 assert!(
5734 names.contains(&"entry/data1".to_string())
5735 && names.contains(&"entry/data2".to_string()),
5736 "both detector datasets must exist; got {:?}",
5737 names
5738 );
5739
5740 let d1 = h5.dataset("entry/data1").unwrap();
5742 assert_eq!(d1.shape(), vec![3, 2, 2], "default dataset extent");
5743 let v1: Vec<u16> = d1.read_raw().unwrap();
5744 assert_eq!(
5745 v1,
5746 vec![10, 10, 10, 10, 13, 13, 13, 13, 14, 14, 14, 14],
5747 "default dataset must hold the default-routed frames in order"
5748 );
5749
5750 let d2 = h5.dataset("entry/data2").unwrap();
5751 assert_eq!(d2.shape(), vec![2, 2, 2], "routed dataset extent");
5752 let v2: Vec<u16> = d2.read_raw().unwrap();
5753 assert_eq!(
5754 v2,
5755 vec![11, 11, 11, 11, 12, 12, 12, 12],
5756 "routed dataset must hold only the frames addressed to it"
5757 );
5758
5759 drop(h5);
5760 std::fs::remove_file(&path).ok();
5761 std::fs::remove_file(&layout).ok();
5762 }
5763
5764 #[test]
5768 fn test_detector_data_destination_non_string_attribute_errors() {
5769 let dir = std::env::temp_dir();
5770 let layout = dir.join("adcore_layout_multidet_err.xml");
5771 std::fs::write(
5772 &layout,
5773 r#"<hdf5_layout>
5774 <global name="detector_data_destination" ndattribute="dest"/>
5775 <group name="entry">
5776 <dataset name="data1" source="detector" det_default="true"/>
5777 <dataset name="data2" source="detector"/>
5778 </group>
5779 </hdf5_layout>"#,
5780 )
5781 .unwrap();
5782
5783 let path = temp_path("hdf5_multidet_err");
5784 let mut writer = Hdf5Writer::new();
5785 writer.store_attributes = false;
5786 assert!(writer.set_layout_filename(layout.to_str().unwrap()));
5787
5788 let mut arr = NDArray::new(
5789 vec![NDDimension::new(2), NDDimension::new(2)],
5790 NDDataType::UInt16,
5791 );
5792 arr.attributes.add(NDAttribute::new_static(
5793 "dest",
5794 "",
5795 NDAttrSource::Driver,
5796 NDAttrValue::Float64(2.0),
5797 ));
5798
5799 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5800 assert!(
5801 writer.write_file(&arr).is_err(),
5802 "a non-string destination attribute must abort the write"
5803 );
5804 writer.close_file().ok();
5805
5806 std::fs::remove_file(&path).ok();
5807 std::fs::remove_file(&layout).ok();
5808 }
5809
5810 #[test]
5811 fn test_layout_hardlink_is_materialised() {
5812 let dir = std::env::temp_dir();
5818 let layout = dir.join("adcore_layout_hardlink.xml");
5819 std::fs::write(
5820 &layout,
5821 r#"<hdf5_layout>
5822 <group name="entry">
5823 <group name="data">
5824 <dataset name="data" source="detector" det_default="true"/>
5825 <hardlink name="data_alias" target="/entry/data/data"/>
5826 </group>
5827 </group>
5828 </hdf5_layout>"#,
5829 )
5830 .unwrap();
5831
5832 let path = temp_path("hdf5_layout_hardlink");
5833 let mut writer = Hdf5Writer::new();
5834 assert!(
5835 writer.set_layout_filename(layout.to_str().unwrap()),
5836 "layout XML must parse: {}",
5837 writer.layout_error
5838 );
5839
5840 let arr = NDArray::new(
5841 vec![NDDimension::new(4), NDDimension::new(4)],
5842 NDDataType::UInt16,
5843 );
5844 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5845 writer.write_file(&arr).unwrap();
5846 writer.close_file().unwrap();
5847
5848 let h5 = H5File::open(&path).unwrap();
5849 let names = h5.dataset_names();
5850 assert!(
5852 names.contains(&"entry/data/data".to_string()),
5853 "image dataset must exist at the layout path; got {:?}",
5854 names
5855 );
5856 assert!(
5858 names.contains(&"entry/data/data_alias".to_string()),
5859 "layout <hardlink> must be materialised as a hard link; got {:?}",
5860 names
5861 );
5862 let alias = h5.dataset("entry/data/data_alias").unwrap();
5864 let orig = h5.dataset("entry/data/data").unwrap();
5865 assert_eq!(alias.shape(), orig.shape());
5866
5867 drop(h5);
5868 std::fs::remove_file(&path).ok();
5869 std::fs::remove_file(&layout).ok();
5870 }
5871
5872 #[test]
5873 fn test_swmr_layout_hardlink_is_materialised() {
5874 let dir = std::env::temp_dir();
5886 let layout = dir.join("adcore_swmr_layout_hardlink.xml");
5887 std::fs::write(
5888 &layout,
5889 r#"<hdf5_layout>
5890 <group name="entry">
5891 <group name="data">
5892 <dataset name="data" source="detector" det_default="true"/>
5893 <hardlink name="data_alias" target="/entry/data/data"/>
5894 </group>
5895 </group>
5896 </hdf5_layout>"#,
5897 )
5898 .unwrap();
5899
5900 let path = temp_path("hdf5_swmr_layout_hardlink");
5901 let mut writer = Hdf5Writer::new();
5902 writer.set_swmr_mode(true);
5903 assert!(
5904 writer.set_layout_filename(layout.to_str().unwrap()),
5905 "layout XML must parse: {}",
5906 writer.layout_error
5907 );
5908
5909 let mut arr = NDArray::new(
5910 vec![NDDimension::new(4), NDDimension::new(4)],
5911 NDDataType::UInt16,
5912 );
5913 if let NDDataBuffer::U16(ref mut v) = arr.data {
5914 for (i, x) in v.iter_mut().enumerate() {
5915 *x = i as u16;
5916 }
5917 }
5918 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5919 assert!(
5920 writer.is_swmr_active(),
5921 "writer must be in SWMR mode for this test"
5922 );
5923 writer.write_file(&arr).unwrap();
5924 writer.write_file(&arr).unwrap();
5925 writer.close_file().unwrap();
5926
5927 let h5 = H5File::open(&path).unwrap();
5928 let names = h5.dataset_names();
5929 assert!(
5931 names.contains(&"entry/data/data".to_string()),
5932 "SWMR image dataset must exist at the nested layout path; got {:?}",
5933 names
5934 );
5935 assert!(
5937 names.contains(&"entry/data/data_alias".to_string()),
5938 "SWMR layout <hardlink> must be materialised as a hard link; got {:?}",
5939 names
5940 );
5941 let alias = h5.dataset("entry/data/data_alias").unwrap();
5943 let orig = h5.dataset("entry/data/data").unwrap();
5944 assert_eq!(alias.shape(), orig.shape());
5945 assert_eq!(orig.shape(), vec![2, 4, 4]);
5946
5947 drop(h5);
5948 std::fs::remove_file(&path).ok();
5949 std::fs::remove_file(&layout).ok();
5950 }
5951
5952 #[test]
5953 fn test_swmr_layout_nested_dataset_placement() {
5954 let dir = std::env::temp_dir();
5961 let layout = dir.join("adcore_swmr_layout_nested.xml");
5962 std::fs::write(
5963 &layout,
5964 r#"<hdf5_layout>
5965 <group name="entry">
5966 <group name="instrument">
5967 <group name="detector">
5968 <dataset name="data" source="detector" det_default="true">
5969 <attribute name="signal" source="constant" value="1" type="int"/>
5970 </dataset>
5971 <hardlink name="data_alias" target="/entry/instrument/detector/data"/>
5972 </group>
5973 </group>
5974 <group name="empty_placeholder"/>
5975 </group>
5976 </hdf5_layout>"#,
5977 )
5978 .unwrap();
5979
5980 let path = temp_path("hdf5_swmr_layout_nested");
5981 let mut writer = Hdf5Writer::new();
5982 writer.set_swmr_mode(true);
5983 assert!(
5984 writer.set_layout_filename(layout.to_str().unwrap()),
5985 "layout XML must parse: {}",
5986 writer.layout_error
5987 );
5988
5989 let mut arr = NDArray::new(
5990 vec![NDDimension::new(4), NDDimension::new(4)],
5991 NDDataType::UInt16,
5992 );
5993 if let NDDataBuffer::U16(ref mut v) = arr.data {
5994 for (i, x) in v.iter_mut().enumerate() {
5995 *x = (i * 3) as u16;
5996 }
5997 }
5998 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5999 assert!(
6000 writer.is_swmr_active(),
6001 "writer must be in SWMR mode for this test"
6002 );
6003 writer.write_file(&arr).unwrap();
6004 writer.write_file(&arr).unwrap();
6005 writer.close_file().unwrap();
6006
6007 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
6010 let names = reader.dataset_names();
6011 assert!(
6013 names.contains(&"entry/instrument/detector/data".to_string()),
6014 "SWMR image dataset must live at the nested layout path; got {:?}",
6015 names
6016 );
6017 assert!(
6018 !names.contains(&"data".to_string()),
6019 "SWMR image dataset must NOT remain at the flat root; got {:?}",
6020 names
6021 );
6022 assert!(
6024 reader.has_group("entry/empty_placeholder"),
6025 "empty layout group must be materialised; groups {:?}",
6026 reader.group_paths()
6027 );
6028 assert!(
6030 names.contains(&"entry/instrument/detector/data_alias".to_string()),
6031 "SWMR layout <hardlink> must resolve to the nested dataset; got {:?}",
6032 names
6033 );
6034 let nested = reader
6035 .dataset_shape("entry/instrument/detector/data")
6036 .unwrap();
6037 let alias = reader
6038 .dataset_shape("entry/instrument/detector/data_alias")
6039 .unwrap();
6040 assert_eq!(nested, vec![2, 4, 4]);
6041 assert_eq!(alias, nested, "hardlink alias must share the target shape");
6042 let via_nested: Vec<u16> = reader
6044 .read_dataset("entry/instrument/detector/data")
6045 .unwrap();
6046 let via_alias: Vec<u16> = reader
6047 .read_dataset("entry/instrument/detector/data_alias")
6048 .unwrap();
6049 assert_eq!(via_nested, via_alias);
6050 assert_eq!(via_nested.len(), 2 * 4 * 4);
6051 let attr_names = reader
6055 .dataset_attr_names("entry/instrument/detector/data")
6056 .unwrap();
6057 for expected in [
6058 "signal",
6059 "NDArrayNumDims",
6060 "NDArrayDimOffset",
6061 "NDArrayDimBinning",
6062 "NDArrayDimReverse",
6063 ] {
6064 assert!(
6065 attr_names.iter().any(|n| n == expected),
6066 "streaming dataset must carry the {expected} attribute; got {attr_names:?}",
6067 );
6068 }
6069
6070 drop(reader);
6071 std::fs::remove_file(&path).ok();
6072 std::fs::remove_file(&layout).ok();
6073 }
6074
6075 #[test]
6076 fn test_default_layout_parses_and_resolves_nexus_paths() {
6077 let layout = Hdf5Writer::default_layout();
6081 assert_eq!(
6082 layout.detector_dataset_path().as_deref(),
6083 Some("/entry/instrument/detector/data")
6084 );
6085 assert_eq!(
6086 layout.ndattr_default_group().as_deref(),
6087 Some("/entry/instrument/NDAttributes")
6088 );
6089 assert_eq!(
6090 layout.dataset_group_path("timestamp").as_deref(),
6091 Some("/entry/instrument/performance")
6092 );
6093 }
6094
6095 #[test]
6096 fn test_no_layout_uses_default_nexus_layout() {
6097 let path = temp_path("hdf5_default_layout");
6102 let mut writer = Hdf5Writer::new();
6103 let arr = NDArray::new(
6104 vec![NDDimension::new(4), NDDimension::new(4)],
6105 NDDataType::UInt8,
6106 );
6107 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6108 writer.write_file(&arr).unwrap();
6109 writer.close_file().unwrap();
6110
6111 let h5 = H5File::open(&path).unwrap();
6112 let det = h5.dataset("entry/instrument/detector/data").unwrap();
6114 assert_eq!(det.shape(), vec![1, 4, 4]);
6115 assert!(
6116 !h5.dataset_names().contains(&"data".to_string()),
6117 "must not write a flat-root `data`; got {:?}",
6118 h5.dataset_names()
6119 );
6120 let linked = h5.dataset("entry/data/data").unwrap();
6122 assert_eq!(linked.shape(), vec![1, 4, 4]);
6123 std::fs::remove_file(&path).ok();
6124 }
6125
6126 #[test]
6127 fn test_default_layout_group_nx_class_attributes() {
6128 let path = temp_path("hdf5_nxclass");
6132 let mut writer = Hdf5Writer::new();
6133 let arr = NDArray::new(
6134 vec![NDDimension::new(4), NDDimension::new(4)],
6135 NDDataType::UInt8,
6136 );
6137 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6138 writer.write_file(&arr).unwrap();
6139 writer.close_file().unwrap();
6140
6141 let h5 = H5File::open(&path).unwrap();
6142 let nx = |g: &str| {
6143 let mut grp = h5.root_group();
6144 for seg in g.split('/') {
6145 grp = grp.group(seg).unwrap();
6146 }
6147 grp.attr_string("NX_class").unwrap()
6148 };
6149 assert_eq!(nx("entry"), "NXentry");
6150 assert_eq!(nx("entry/instrument"), "NXinstrument");
6151 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6152 assert_eq!(nx("entry/instrument/NDAttributes"), "NXcollection");
6153 assert_eq!(nx("entry/instrument/detector/NDAttributes"), "NXcollection");
6154 assert_eq!(nx("entry/data"), "NXdata");
6155 std::fs::remove_file(&path).ok();
6156 }
6157
6158 #[test]
6159 fn test_swmr_default_layout_group_nx_class_attributes() {
6160 let path = temp_path("hdf5_swmr_nxclass");
6163 let mut writer = Hdf5Writer::new();
6164 writer.set_swmr_mode(true);
6165 let arr = NDArray::new(
6166 vec![NDDimension::new(4), NDDimension::new(4)],
6167 NDDataType::Float32,
6168 );
6169 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6170 writer.write_file(&arr).unwrap();
6171 writer.close_file().unwrap();
6172
6173 let h5 = H5File::open(&path).unwrap();
6174 let nx = |g: &str| {
6175 let mut grp = h5.root_group();
6176 for seg in g.split('/') {
6177 grp = grp.group(seg).unwrap();
6178 }
6179 grp.attr_string("NX_class").unwrap()
6180 };
6181 assert_eq!(nx("entry"), "NXentry");
6182 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6183 assert_eq!(nx("entry/data"), "NXdata");
6184 std::fs::remove_file(&path).ok();
6185 }
6186
6187 fn ramp_u16(y: usize, x: usize) -> NDArray {
6192 let data: Vec<u16> = (0..(y * x) as u16).collect();
6193 NDArray::with_data(
6194 vec![NDDimension::new(y), NDDimension::new(x)],
6195 NDDataBuffer::U16(data),
6196 )
6197 }
6198
6199 fn u16_pixels(arr: &NDArray) -> Vec<u16> {
6200 match &arr.data {
6201 NDDataBuffer::U16(v) => v.clone(),
6202 _ => unreachable!("expected U16 buffer"),
6203 }
6204 }
6205
6206 #[test]
6207 fn test_codec_chunk_bytes_lz4_prepends_hdf5_header() {
6208 let codec = Codec {
6213 name: CodecName::LZ4,
6214 compressed_size: 4,
6215 level: 0,
6216 shuffle: 0,
6217 compressor: 0,
6218 original_data_type: NDDataType::UInt16,
6219 };
6220 let payload = [0xAAu8, 0xBB, 0xCC, 0xDD];
6221 let out = codec_chunk_bytes(&codec, 32, &payload);
6222 let mut expect = Vec::new();
6223 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);
6227 assert_eq!(out, expect);
6228 }
6229
6230 #[test]
6231 fn test_codec_chunk_bytes_verbatim_for_self_describing() {
6232 for name in [CodecName::Blosc, CodecName::JPEG] {
6236 let codec = Codec {
6237 name,
6238 compressed_size: 3,
6239 level: 0,
6240 shuffle: 0,
6241 compressor: 0,
6242 original_data_type: NDDataType::UInt8,
6243 };
6244 let payload = [1u8, 2, 3];
6245 assert_eq!(codec_chunk_bytes(&codec, 99, &payload), payload.to_vec());
6246 }
6247 }
6248
6249 #[test]
6250 fn test_codecs_match() {
6251 let a = Codec {
6252 name: CodecName::LZ4,
6253 compressed_size: 1,
6254 level: 0,
6255 shuffle: 0,
6256 compressor: 0,
6257 original_data_type: NDDataType::UInt8,
6258 };
6259 assert!(codecs_match(None, None));
6260 assert!(!codecs_match(Some(&a), None));
6261 assert!(!codecs_match(None, Some(&a)));
6262 assert!(codecs_match(Some(&a), Some(&a.clone())));
6263 let mut diff = a.clone();
6264 diff.compressor = 1;
6265 assert!(!codecs_match(Some(&a), Some(&diff)));
6266 let mut other_type = a.clone();
6268 other_type.original_data_type = NDDataType::Float64;
6269 assert!(codecs_match(Some(&a), Some(&other_type)));
6270 }
6271
6272 #[test]
6273 fn test_codec_filter_pipeline_ids() {
6274 let w = Hdf5Writer::new();
6275 let mk = |name| Codec {
6276 name,
6277 compressed_size: 0,
6278 level: 5,
6279 shuffle: 1,
6280 compressor: 2,
6281 original_data_type: NDDataType::UInt16,
6282 };
6283 assert_eq!(
6284 w.codec_filter_pipeline(&mk(CodecName::LZ4))
6285 .unwrap()
6286 .filters[0]
6287 .id,
6288 FILTER_LZ4
6289 );
6290 assert_eq!(
6291 w.codec_filter_pipeline(&mk(CodecName::BSLZ4))
6292 .unwrap()
6293 .filters[0]
6294 .id,
6295 FILTER_BSHUF
6296 );
6297 let blosc = w.codec_filter_pipeline(&mk(CodecName::Blosc)).unwrap();
6298 assert_eq!(blosc.filters[0].id, FILTER_BLOSC);
6299 assert_eq!(blosc.filters[0].cd_values[4], 5, "level");
6302 assert_eq!(blosc.filters[0].cd_values[5], 1, "shuffle");
6303 assert_eq!(blosc.filters[0].cd_values[6], 2, "compressor");
6304 assert_eq!(
6305 w.codec_filter_pipeline(&mk(CodecName::JPEG))
6306 .unwrap()
6307 .filters[0]
6308 .id,
6309 FILTER_JPEG
6310 );
6311 assert!(w.codec_filter_pipeline(&mk(CodecName::None)).is_none());
6313 assert!(w.codec_filter_pipeline(&mk(CodecName::Zlib)).is_none());
6314 assert!(w.codec_filter_pipeline(&mk(CodecName::LZ4HDF5)).is_none());
6315 }
6316
6317 #[test]
6318 fn test_compression_aware_true() {
6319 assert!(Hdf5FileProcessor::new().compression_aware());
6321 }
6322
6323 #[test]
6324 fn test_direct_chunk_write_lz4_roundtrips() {
6325 let path = temp_path("hdf5_dcw_lz4");
6326 let mut writer = Hdf5Writer::new();
6327 let orig = ramp_u16(4, 5);
6328 let expect = u16_pixels(&orig);
6329 let comp = crate::codec::compress_lz4(&orig);
6330 assert!(comp.codec.is_some());
6331 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6332 writer.write_file(&comp).unwrap();
6333 writer.close_file().unwrap();
6334
6335 let h5 = H5File::open(&path).unwrap();
6336 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6337 assert_eq!(ds.shape(), vec![1, 5, 4]);
6341 assert!(ds.is_chunked());
6342 let read = ds.read_raw::<u16>().unwrap();
6343 assert_eq!(
6344 read, expect,
6345 "LZ4 direct-chunk-write must reverse to the original pixels"
6346 );
6347 std::fs::remove_file(&path).ok();
6348 }
6349
6350 #[test]
6351 fn test_direct_chunk_write_blosc_roundtrips() {
6352 let path = temp_path("hdf5_dcw_blosc");
6353 let mut writer = Hdf5Writer::new();
6354 let orig = ramp_u16(4, 5);
6355 let expect = u16_pixels(&orig);
6356 let comp = crate::codec::compress_blosc(&orig, &crate::codec::BloscConfig::default());
6357 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Blosc);
6358 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6359 writer.write_file(&comp).unwrap();
6360 writer.close_file().unwrap();
6361
6362 let h5 = H5File::open(&path).unwrap();
6363 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6364 assert_eq!(ds.shape(), vec![1, 5, 4]);
6365 let read = ds.read_raw::<u16>().unwrap();
6366 assert_eq!(read, expect, "BLOSC direct-chunk-write must round-trip");
6367 std::fs::remove_file(&path).ok();
6368 }
6369
6370 #[test]
6371 fn test_direct_chunk_write_bslz4_roundtrips() {
6372 let path = temp_path("hdf5_dcw_bslz4");
6381 let mut writer = Hdf5Writer::new();
6382 let orig = ramp_u16(128, 128);
6383 let expect = u16_pixels(&orig);
6384 let comp = crate::codec::compress_bslz4(&orig);
6385 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::BSLZ4);
6386 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6387 writer.write_file(&comp).unwrap();
6388 writer.close_file().unwrap();
6389
6390 let h5 = H5File::open(&path).unwrap();
6391 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6392 assert_eq!(ds.shape(), vec![1, 128, 128]);
6393 assert!(ds.is_chunked());
6394 assert_eq!(ds.element_size(), 2, "dataset keeps the original u16 type");
6395 let read = ds.read_raw::<u16>().unwrap();
6396 assert_eq!(read, expect, "BSLZ4 direct-chunk-write must round-trip");
6397 std::fs::remove_file(&path).ok();
6398 }
6399
6400 #[test]
6401 fn test_codec_chunk_bytes_bslz4_header() {
6402 let codec = Codec {
6407 name: CodecName::BSLZ4,
6408 compressed_size: 0,
6409 level: 0,
6410 shuffle: 0,
6411 compressor: 0,
6412 original_data_type: NDDataType::UInt16,
6413 };
6414 let payload = [0xDEu8, 0xAD, 0xBE, 0xEF];
6415 let total = 128 * 128 * 2; let out = codec_chunk_bytes(&codec, total, &payload);
6417 assert_eq!(&out[0..8], &(total as u64).to_be_bytes());
6418 assert_eq!(&out[8..12], &8192u32.to_be_bytes());
6420 assert_eq!(&out[12..], &payload, "canonical stream follows verbatim");
6421 }
6422
6423 #[test]
6424 fn test_direct_chunk_write_lz4_multiframe_extends_leading_axis() {
6425 let path = temp_path("hdf5_dcw_lz4_multi");
6426 let mut writer = Hdf5Writer::new();
6427 let frames: Vec<NDArray> = (0..3u16)
6428 .map(|f| {
6429 let data: Vec<u16> = (0..20u16).map(|i| i + f * 100).collect();
6430 NDArray::with_data(
6431 vec![NDDimension::new(4), NDDimension::new(5)],
6432 NDDataBuffer::U16(data),
6433 )
6434 })
6435 .collect();
6436 let comp: Vec<NDArray> = frames.iter().map(crate::codec::compress_lz4).collect();
6437 writer
6438 .open_file(&path, NDFileMode::Stream, &comp[0])
6439 .unwrap();
6440 for c in &comp {
6441 writer.write_file(c).unwrap();
6442 }
6443 writer.close_file().unwrap();
6444
6445 let h5 = H5File::open(&path).unwrap();
6446 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6447 assert_eq!(ds.shape(), vec![3, 5, 4], "three compressed frames stacked");
6448 let read = ds.read_raw::<u16>().unwrap();
6449 let mut expect = Vec::new();
6450 for f in 0..3u16 {
6451 for i in 0..20u16 {
6452 expect.push(i + f * 100);
6453 }
6454 }
6455 assert_eq!(read, expect, "every frame's pixels recovered in order");
6456 std::fs::remove_file(&path).ok();
6457 }
6458
6459 #[test]
6460 fn test_direct_chunk_write_jpeg_records_chunked_dataset() {
6461 let path = temp_path("hdf5_dcw_jpeg");
6466 let mut writer = Hdf5Writer::new();
6467 let mono: Vec<u8> = (0..64u16).map(|i| (i * 3) as u8).collect();
6468 let src = NDArray::with_data(
6469 vec![NDDimension::new(8), NDDimension::new(8)],
6470 NDDataBuffer::U8(mono),
6471 );
6472 let comp = crate::codec::compress_jpeg(&src, 90).expect("jpeg encode");
6473 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::JPEG);
6474 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6475 writer.write_file(&comp).unwrap();
6476 writer.close_file().unwrap();
6477
6478 let h5 = H5File::open(&path).unwrap();
6479 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6480 assert_eq!(ds.shape(), vec![1, 8, 8]);
6481 assert!(ds.is_chunked());
6482 std::fs::remove_file(&path).ok();
6483 }
6484
6485 #[test]
6486 fn test_codec_change_mid_stream_errors() {
6487 let path = temp_path("hdf5_codec_change");
6491 let mut writer = Hdf5Writer::new();
6492 let f0 = crate::codec::compress_lz4(&ramp_u16(4, 5));
6493 let f1_plain = ramp_u16(4, 5); writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
6495 writer.write_file(&f0).unwrap();
6496 assert!(
6497 writer.write_file(&f1_plain).is_err(),
6498 "an uncompressed frame must be rejected for a compressed dataset"
6499 );
6500 writer.close_file().ok();
6501 std::fs::remove_file(&path).ok();
6502 }
6503
6504 #[test]
6505 fn test_swmr_rejects_compressed_array() {
6506 let path = temp_path("hdf5_swmr_compressed");
6509 let mut writer = Hdf5Writer::new();
6510 writer.set_swmr_mode(true);
6511 let comp = crate::codec::compress_lz4(&ramp_u16(4, 5));
6512 writer.open_file(&path, NDFileMode::Stream, &comp).unwrap();
6513 assert!(
6514 writer.write_file(&comp).is_err(),
6515 "SWMR mode cannot direct-chunk-write a pre-compressed array"
6516 );
6517 writer.close_file().ok();
6518 std::fs::remove_file(&path).ok();
6519 }
6520
6521 #[test]
6522 fn test_unsupported_codec_rejected_on_open() {
6523 let path = temp_path("hdf5_dcw_zlib");
6526 let mut writer = Hdf5Writer::new();
6527 let comp = crate::codec::compress_zlib(&ramp_u16(4, 5));
6528 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Zlib);
6529 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6530 assert!(
6531 writer.write_file(&comp).is_err(),
6532 "an unsupported (zlib) codec must be rejected, not silently mis-written"
6533 );
6534 writer.close_file().ok();
6535 std::fs::remove_file(&path).ok();
6536 }
6537}