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