1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrDataType, NDAttrValue};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
11};
12
13use rust_hdf5::H5File;
14use rust_hdf5::format::messages::filter::{
15 FILTER_BLOSC, FILTER_BSHUF, FILTER_JPEG, FILTER_NBIT, FILTER_SZIP, Filter, FilterPipeline,
16};
17use rust_hdf5::swmr::SwmrFileWriter;
18
19use crate::hdf5_layout::Hdf5Layout;
20
21const COMPRESS_NONE: i32 = 0;
23const COMPRESS_NBIT: i32 = 1;
24const COMPRESS_SZIP: i32 = 2;
25const COMPRESS_ZLIB: i32 = 3;
26const COMPRESS_BLOSC: i32 = 4;
27const COMPRESS_BSHUF: i32 = 5;
28const COMPRESS_LZ4: i32 = 6;
29const COMPRESS_JPEG: i32 = 7;
30
31const BLOSC_LZ: i32 = 0;
33const BLOSC_LZ4: i32 = 1;
34const BLOSC_LZ4HC: i32 = 2;
35const BLOSC_SNAPPY: i32 = 3;
36const BLOSC_ZLIB: i32 = 4;
37const BLOSC_ZSTD: i32 = 5;
38
39const MAX_EXTRA_DIMS: usize = 10;
41
42const DTYPE_ATTR: &str = "NDArrayDataType";
45
46#[derive(Clone)]
48struct ChunkConfig {
49 auto: bool,
52 n_row_chunks: usize,
53 n_col_chunks: usize,
54 n_frames_chunks: usize,
55 ndattr_chunk: usize,
57}
58
59impl Default for ChunkConfig {
60 fn default() -> Self {
61 Self {
62 auto: true,
63 n_row_chunks: 0,
64 n_col_chunks: 0,
65 n_frames_chunks: 1,
66 ndattr_chunk: 16,
67 }
68 }
69}
70
71#[derive(Clone, Default)]
73struct ExtraDim {
74 size: usize,
75 name: String,
76}
77
78struct AttributeDataset {
82 name: String,
83 data_type: NDAttrDataType,
84 buffer: Vec<u8>,
86 frames: usize,
87}
88
89impl AttributeDataset {
90 fn new(name: String, data_type: NDAttrDataType) -> Self {
91 Self {
92 name,
93 data_type,
94 buffer: Vec::new(),
95 frames: 0,
96 }
97 }
98
99 fn element_size(&self) -> usize {
102 match self.data_type {
103 NDAttrDataType::Int8 | NDAttrDataType::UInt8 => 1,
104 NDAttrDataType::Int16 | NDAttrDataType::UInt16 => 2,
105 NDAttrDataType::Int32 | NDAttrDataType::UInt32 | NDAttrDataType::Float32 => 4,
106 NDAttrDataType::Int64 | NDAttrDataType::UInt64 | NDAttrDataType::Float64 => 8,
107 NDAttrDataType::String => MAX_ATTRIBUTE_STRING_SIZE,
108 }
109 }
110
111 fn push(&mut self, value: &NDAttrValue) {
113 let es = self.element_size();
114 let mut bytes = vec![0u8; es];
115 match self.data_type {
116 NDAttrDataType::Int8 => bytes[0] = value.as_i64().unwrap_or(0) as i8 as u8,
117 NDAttrDataType::UInt8 => bytes[0] = value.as_i64().unwrap_or(0) as u8,
118 NDAttrDataType::Int16 => {
119 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i16).to_le_bytes())
120 }
121 NDAttrDataType::UInt16 => {
122 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u16).to_le_bytes())
123 }
124 NDAttrDataType::Int32 => {
125 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i32).to_le_bytes())
126 }
127 NDAttrDataType::UInt32 => {
128 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u32).to_le_bytes())
129 }
130 NDAttrDataType::Int64 => {
131 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0)).to_le_bytes())
132 }
133 NDAttrDataType::UInt64 => {
134 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u64).to_le_bytes())
135 }
136 NDAttrDataType::Float32 => {
137 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0) as f32).to_le_bytes())
138 }
139 NDAttrDataType::Float64 => {
140 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0)).to_le_bytes())
141 }
142 NDAttrDataType::String => {
143 let s = value.as_string();
144 let src = s.as_bytes();
145 let n = src.len().min(es - 1);
146 bytes[..n].copy_from_slice(&src[..n]);
147 }
148 }
149 self.buffer.extend_from_slice(&bytes);
150 self.frames += 1;
151 }
152}
153
154const MAX_ATTRIBUTE_STRING_SIZE: usize = 256;
157
158enum Hdf5Handle {
160 Standard {
161 file: H5File,
162 primary: Option<rust_hdf5::H5Dataset>,
166 },
167 Swmr {
168 writer: Box<SwmrFileWriter>,
170 ds_index: usize,
171 compression_dropped: bool,
174 },
175}
176
177pub struct Hdf5Writer {
179 current_path: Option<PathBuf>,
180 handle: Option<Hdf5Handle>,
181 frame_count: usize,
182 frame_band: Vec<Vec<u8>>,
186 dataset_name: String,
187 open_data_type: Option<NDDataType>,
189 open_frame_dims: Option<Vec<usize>>,
191 open_extra_extent: Option<usize>,
195 compression_type: i32,
197 z_compress_level: u32,
198 szip_num_pixels: u32,
199 nbit_precision: u32,
200 nbit_offset: u32,
201 jpeg_quality: u32,
202 blosc_shuffle_type: i32,
203 blosc_compressor: i32,
204 blosc_compress_level: u32,
205 chunk: ChunkConfig,
207 n_extra_dims: usize,
208 extra_dims: [ExtraDim; MAX_EXTRA_DIMS],
209 fill_value: f64,
210 dim_att_datasets: bool,
211 swmr_mode: bool,
213 flush_nth_frame: usize,
214 pub swmr_cb_counter: u32,
215 pub store_attributes: bool,
217 pub store_performance: bool,
218 pub total_runtime: f64,
219 pub total_bytes: u64,
220 perf_rows: Vec<[f64; 5]>,
223 perf_prev: Option<std::time::Instant>,
224 perf_first: Option<std::time::Instant>,
225 attr_datasets: Vec<AttributeDataset>,
227 layout_filename: Option<PathBuf>,
229 layout: Option<Hdf5Layout>,
230 pub layout_valid: bool,
231 pub layout_error: String,
232 resolved_dataset_path: String,
237 resolved_ndattr_group: String,
240 resolved_perf_group: String,
242}
243
244impl Hdf5Writer {
245 pub fn new() -> Self {
246 Self {
247 current_path: None,
248 handle: None,
249 frame_count: 0,
250 frame_band: Vec::new(),
251 dataset_name: "data".to_string(),
252 open_data_type: None,
253 open_frame_dims: None,
254 open_extra_extent: None,
255 compression_type: 0,
256 z_compress_level: 6,
257 szip_num_pixels: 16,
258 nbit_precision: 0,
259 nbit_offset: 0,
260 jpeg_quality: 90,
261 blosc_shuffle_type: 0,
262 blosc_compressor: 0,
263 blosc_compress_level: 5,
264 chunk: ChunkConfig::default(),
265 n_extra_dims: 0,
266 extra_dims: Default::default(),
267 fill_value: 0.0,
268 dim_att_datasets: false,
269 swmr_mode: false,
270 flush_nth_frame: 0,
271 swmr_cb_counter: 0,
272 store_attributes: true,
273 store_performance: false,
274 total_runtime: 0.0,
275 total_bytes: 0,
276 perf_rows: Vec::new(),
277 perf_prev: None,
278 perf_first: None,
279 attr_datasets: Vec::new(),
280 layout_filename: None,
281 layout: None,
282 layout_valid: false,
283 layout_error: String::new(),
284 resolved_dataset_path: "data".to_string(),
285 resolved_ndattr_group: String::new(),
286 resolved_perf_group: String::new(),
287 }
288 }
289
290 pub fn set_dataset_name(&mut self, name: &str) {
291 self.dataset_name = name.to_string();
292 }
293
294 pub fn set_compression_type(&mut self, v: i32) {
295 self.compression_type = v;
296 }
297
298 pub fn set_z_compress_level(&mut self, v: u32) {
299 self.z_compress_level = v;
300 }
301
302 pub fn set_szip_num_pixels(&mut self, v: u32) {
303 self.szip_num_pixels = v;
304 }
305
306 pub fn set_blosc_shuffle_type(&mut self, v: i32) {
307 self.blosc_shuffle_type = v;
308 }
309
310 pub fn set_blosc_compressor(&mut self, v: i32) {
311 self.blosc_compressor = v;
312 }
313
314 pub fn set_blosc_compress_level(&mut self, v: u32) {
315 self.blosc_compress_level = v;
316 }
317
318 pub fn set_nbit_precision(&mut self, v: u32) {
319 self.nbit_precision = v;
320 }
321
322 pub fn set_nbit_offset(&mut self, v: u32) {
323 self.nbit_offset = v;
324 }
325
326 pub fn set_jpeg_quality(&mut self, v: u32) {
327 self.jpeg_quality = v;
328 }
329
330 pub fn set_store_attributes(&mut self, v: bool) {
331 self.store_attributes = v;
332 }
333
334 pub fn set_store_performance(&mut self, v: bool) {
335 self.store_performance = v;
336 }
337
338 pub fn set_swmr_mode(&mut self, v: bool) {
339 self.swmr_mode = v;
340 }
341
342 pub fn set_flush_nth_frame(&mut self, v: usize) {
343 self.flush_nth_frame = v;
344 }
345
346 pub fn set_chunk_size_auto(&mut self, v: bool) {
347 self.chunk.auto = v;
348 }
349
350 pub fn set_n_row_chunks(&mut self, v: usize) {
351 self.chunk.n_row_chunks = v;
352 }
353
354 pub fn set_n_col_chunks(&mut self, v: usize) {
355 self.chunk.n_col_chunks = v;
356 }
357
358 pub fn set_n_frames_chunks(&mut self, v: usize) {
359 self.chunk.n_frames_chunks = v;
360 }
361
362 pub fn set_ndattr_chunk(&mut self, v: usize) {
363 self.chunk.ndattr_chunk = v.max(1);
364 }
365
366 pub fn set_n_extra_dims(&mut self, v: usize) {
367 self.n_extra_dims = v.min(MAX_EXTRA_DIMS);
368 }
369
370 pub fn set_extra_dim_size(&mut self, idx: usize, size: usize) {
371 if idx < MAX_EXTRA_DIMS {
372 self.extra_dims[idx].size = size;
373 }
374 }
375
376 pub fn set_extra_dim_name(&mut self, idx: usize, name: &str) {
377 if idx < MAX_EXTRA_DIMS {
378 self.extra_dims[idx].name = name.to_string();
379 }
380 }
381
382 pub fn set_fill_value(&mut self, v: f64) {
383 self.fill_value = v;
384 }
385
386 pub fn set_dim_att_datasets(&mut self, v: bool) {
387 self.dim_att_datasets = v;
388 }
389
390 pub fn set_layout_filename(&mut self, path: &str) -> bool {
393 if path.trim().is_empty() {
394 self.layout_filename = None;
395 self.layout = None;
396 self.layout_valid = false;
397 self.layout_error.clear();
398 return true;
399 }
400 let p = PathBuf::from(path);
401 match Hdf5Layout::from_file(&p) {
402 Ok(layout) => {
403 self.layout_filename = Some(p);
404 self.layout = Some(layout);
405 self.layout_valid = true;
406 self.layout_error.clear();
407 true
408 }
409 Err(e) => {
410 self.layout_filename = Some(p);
411 self.layout = None;
412 self.layout_valid = false;
413 self.layout_error = e.0;
414 false
415 }
416 }
417 }
418
419 pub fn frame_count(&self) -> usize {
420 self.frame_count
421 }
422
423 pub fn flush_swmr(&mut self) {
425 if let Some(Hdf5Handle::Swmr { ref mut writer, .. }) = self.handle {
426 if writer.flush().is_ok() {
427 self.swmr_cb_counter += 1;
428 }
429 }
430 }
431
432 pub fn is_swmr_active(&self) -> bool {
434 matches!(self.handle, Some(Hdf5Handle::Swmr { .. }))
435 }
436
437 pub fn swmr_compression_dropped(&self) -> bool {
439 matches!(
440 self.handle,
441 Some(Hdf5Handle::Swmr {
442 compression_dropped: true,
443 ..
444 })
445 )
446 }
447
448 fn build_pipeline(&self, element_size: usize) -> Option<FilterPipeline> {
450 match self.compression_type {
451 COMPRESS_NONE => None,
452 COMPRESS_ZLIB => Some(FilterPipeline::deflate(self.z_compress_level)),
453 COMPRESS_SZIP => Some(FilterPipeline {
454 filters: vec![Filter {
455 id: FILTER_SZIP,
456 flags: 0,
457 cd_values: vec![4, self.szip_num_pixels],
458 }],
459 }),
460 COMPRESS_LZ4 => Some(FilterPipeline::lz4()),
461 COMPRESS_BSHUF => Some(FilterPipeline {
462 filters: vec![Filter {
466 id: FILTER_BSHUF,
467 flags: 0,
468 cd_values: vec![0, 0, element_size as u32, 0, 2],
469 }],
470 }),
471 COMPRESS_BLOSC => {
472 let compressor_code = match self.blosc_compressor {
473 BLOSC_LZ => 0,
474 BLOSC_LZ4 => 1,
475 BLOSC_LZ4HC => 2,
476 BLOSC_SNAPPY => 3,
477 BLOSC_ZLIB => 4,
478 BLOSC_ZSTD => 5,
479 _ => 0,
480 };
481 Some(FilterPipeline {
482 filters: vec![Filter {
483 id: FILTER_BLOSC,
484 flags: 0,
485 cd_values: vec![
486 2,
487 2,
488 element_size as u32,
489 0,
490 self.blosc_compress_level,
491 self.blosc_shuffle_type as u32,
492 compressor_code,
493 ],
494 }],
495 })
496 }
497 COMPRESS_NBIT => {
498 if self.nbit_precision > 0 {
499 Some(FilterPipeline {
500 filters: vec![Filter {
501 id: FILTER_NBIT,
502 flags: 0,
503 cd_values: vec![self.nbit_precision, self.nbit_offset],
504 }],
505 })
506 } else {
507 None
508 }
509 }
510 COMPRESS_JPEG => Some(FilterPipeline {
511 filters: vec![Filter {
512 id: FILTER_JPEG,
513 flags: 0,
514 cd_values: vec![self.jpeg_quality],
515 }],
516 }),
517 _ => None,
518 }
519 }
520
521 fn primary_layout(&self, frame_dims: &[usize]) -> (Vec<usize>, Vec<usize>, Option<usize>) {
545 let extra_extent = if self.n_extra_dims > 0 {
546 Some(
547 (0..self.n_extra_dims)
548 .map(|i| self.extra_dims[i].size.max(1))
549 .product::<usize>(),
550 )
551 } else {
552 None
553 };
554
555 let mut shape: Vec<usize> = Vec::new();
556 shape.push(extra_extent.unwrap_or(1));
558 shape.extend_from_slice(frame_dims);
559
560 let ndims = shape.len();
561 let mut chunk = vec![1usize; ndims];
562 chunk[0] = if extra_extent.is_some() {
565 1
566 } else {
567 self.chunk.n_frames_chunks.max(1)
568 };
569 if frame_dims.len() == 2 {
570 let y = frame_dims[0].max(1);
572 let x = frame_dims[1].max(1);
573 chunk[1] = Self::clamp_chunk(self.chunk.n_row_chunks, y, self.chunk.auto);
574 chunk[2] = Self::clamp_chunk(self.chunk.n_col_chunks, x, self.chunk.auto);
575 } else {
576 for (i, &d) in frame_dims.iter().enumerate() {
578 chunk[1 + i] = d.max(1);
579 }
580 }
581 (shape, chunk, extra_extent)
582 }
583
584 fn clamp_chunk(requested: usize, dim: usize, auto: bool) -> usize {
587 if auto || requested == 0 || requested > dim {
588 dim
589 } else {
590 requested
591 }
592 }
593
594 fn flush_band(
603 ds: &rust_hdf5::H5Dataset,
604 band_idx: usize,
605 frames: &[Vec<u8>],
606 frame_dims: &[usize],
607 chunk: &[usize],
608 elem_size: usize,
609 ) -> ADResult<()> {
610 let fc = chunk[0];
611 if frame_dims.len() != 2 {
614 let frame_len = frame_dims.iter().product::<usize>() * elem_size;
615 let mut buf = vec![0u8; fc * frame_len];
616 for (f, fb) in frames.iter().take(fc).enumerate() {
617 buf[f * frame_len..f * frame_len + frame_len].copy_from_slice(fb);
618 }
619 let mut coords = vec![0usize; chunk.len()];
620 coords[0] = band_idx;
621 return ds.write_chunk_at(&coords, &buf).map_err(|e| {
622 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
623 });
624 }
625
626 let (y, x) = (frame_dims[0], frame_dims[1]);
627 let (rc, cc) = (chunk[1], chunk[2]);
628 let row_tiles = y.div_ceil(rc);
629 let col_tiles = x.div_ceil(cc);
630 for ry in 0..row_tiles {
631 for cx in 0..col_tiles {
632 let mut tile = vec![0u8; fc * rc * cc * elem_size];
633 for f in 0..fc {
634 let Some(fb) = frames.get(f) else {
635 break; };
637 for r in 0..rc {
638 let sy = ry * rc + r;
639 if sy >= y {
640 break;
641 }
642 for c in 0..cc {
643 let sx = cx * cc + c;
644 if sx >= x {
645 break;
646 }
647 let src = (sy * x + sx) * elem_size;
648 let dst = ((f * rc + r) * cc + c) * elem_size;
649 tile[dst..dst + elem_size].copy_from_slice(&fb[src..src + elem_size]);
650 }
651 }
652 }
653 ds.write_chunk_at(&[band_idx, ry, cx], &tile).map_err(|e| {
654 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
655 })?;
656 }
657 }
658 Ok(())
659 }
660
661 fn finalize_standard_primary(&mut self) -> ADResult<()> {
664 let Some(frame_dims) = self.open_frame_dims.clone() else {
665 return Ok(());
666 };
667 let (_, chunk, extra_extent) = self.primary_layout(&frame_dims);
668 let elem_size = self.open_data_type.map(|t| t.element_size()).unwrap_or(1);
669 let total = self.frame_count;
670 let fc = chunk[0];
671 {
672 let ds = match &self.handle {
673 Some(Hdf5Handle::Standard {
674 primary: Some(ds), ..
675 }) => ds,
676 _ => return Ok(()),
677 };
678 if !self.frame_band.is_empty() {
679 let band_idx = total.saturating_sub(1) / fc;
680 Self::flush_band(
681 ds,
682 band_idx,
683 &self.frame_band,
684 &frame_dims,
685 &chunk,
686 elem_size,
687 )?;
688 }
689 if extra_extent.is_none() && total > 0 {
693 let mut dims = vec![total];
694 dims.extend_from_slice(&frame_dims);
695 ds.set_extent(&dims).map_err(|e| {
696 ADError::UnsupportedConversion(format!("HDF5 set_extent error: {}", e))
697 })?;
698 }
699 }
700 self.frame_band.clear();
701 Ok(())
702 }
703
704 fn open_swmr(&mut self, path: &Path, array: &NDArray) -> ADResult<()> {
717 let mut swmr = SwmrFileWriter::create(path)
718 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR create error: {}", e)))?;
719
720 let frame_dims: Vec<u64> = array.dims.iter().rev().map(|d| d.size as u64).collect();
721
722 let element_size = array.data.data_type().element_size();
728 let pipeline = self.build_pipeline(element_size);
729 let chunk: Vec<u64> = {
730 let usize_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
731 let (_, c, _) = self.primary_layout(&usize_dims);
732 c.iter().map(|&v| v as u64).collect()
733 };
734
735 let ds_group_path: Option<String> = self
743 .resolved_dataset_path
744 .rsplit_once('/')
745 .map(|(group_path, _leaf)| group_path.to_string());
746 let ds_name = self.resolved_dataset_path.clone();
747
748 macro_rules! create_ds {
749 ($t:ty) => {
750 match pipeline.clone() {
751 Some(pl) => swmr
752 .create_streaming_dataset_chunked_compressed::<$t>(
753 &ds_name,
754 &frame_dims,
755 &chunk,
756 pl,
757 )
758 .map_err(|e| {
759 ADError::UnsupportedConversion(format!(
760 "SWMR create compressed dataset error: {}",
761 e
762 ))
763 }),
764 None => swmr
765 .create_streaming_dataset_chunked::<$t>(&ds_name, &frame_dims, &chunk)
766 .map_err(|e| {
767 ADError::UnsupportedConversion(format!(
768 "SWMR create dataset error: {}",
769 e
770 ))
771 }),
772 }
773 };
774 }
775
776 let ds_index = match array.data.data_type() {
777 NDDataType::Int8 => create_ds!(i8)?,
778 NDDataType::UInt8 => create_ds!(u8)?,
779 NDDataType::Int16 => create_ds!(i16)?,
780 NDDataType::UInt16 => create_ds!(u16)?,
781 NDDataType::Int32 => create_ds!(i32)?,
782 NDDataType::UInt32 => create_ds!(u32)?,
783 NDDataType::Int64 => create_ds!(i64)?,
784 NDDataType::UInt64 => create_ds!(u64)?,
785 NDDataType::Float32 => create_ds!(f32)?,
786 NDDataType::Float64 => create_ds!(f64)?,
787 };
788
789 self.build_swmr_layout_groups(&mut swmr)?;
796 if let Some(ref group_path) = ds_group_path {
797 let abs_group = format!("/{}", group_path);
800 swmr.assign_dataset_to_group(&abs_group, ds_index)
801 .map_err(|e| {
802 ADError::UnsupportedConversion(format!(
803 "SWMR assign dataset to group '{}': {}",
804 abs_group, e
805 ))
806 })?;
807 }
808 self.write_swmr_layout_dataset_attrs(&mut swmr, ds_index)?;
809 self.build_swmr_layout_hardlinks(&mut swmr)?;
810
811 swmr.start_swmr()
812 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR start error: {}", e)))?;
813
814 let compression_dropped = self.compression_type != COMPRESS_NONE && pipeline.is_none();
819 if compression_dropped {
820 eprintln!(
821 "NDFileHDF5: WARNING — SWMR mode requested compression type {} \
822 but no filter pipeline could be built for it; the SWMR file \
823 will be written UNCOMPRESSED.",
824 self.compression_type
825 );
826 }
827
828 self.handle = Some(Hdf5Handle::Swmr {
829 writer: Box::new(swmr),
830 ds_index,
831 compression_dropped,
832 });
833 self.open_data_type = Some(array.data.data_type());
834 self.open_frame_dims = Some(array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>());
835 Ok(())
836 }
837
838 fn build_swmr_layout_groups(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
847 let layout = match self.layout.as_ref() {
848 Some(l) => l,
849 None => return Ok(()),
850 };
851 fn collect(g: &crate::hdf5_layout::LayoutGroup, prefix: &str, out: &mut Vec<String>) {
852 let here = if prefix.is_empty() {
853 g.name.clone()
854 } else {
855 format!("{}/{}", prefix, g.name)
856 };
857 out.push(here.clone());
858 for sub in &g.groups {
859 collect(sub, &here, out);
860 }
861 }
862 let mut paths = Vec::new();
863 for g in &layout.groups {
864 collect(g, "", &mut paths);
865 }
866 paths.sort_by_key(|p| p.matches('/').count());
867 paths.dedup();
868 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
869 for path in &paths {
870 if created.contains(path) {
871 continue;
872 }
873 let (parent, leaf) = match path.rsplit_once('/') {
874 Some((p, l)) => (format!("/{}", p), l),
875 None => ("/".to_string(), path.as_str()),
876 };
877 swmr.create_group(&parent, leaf).map_err(|e| {
878 ADError::UnsupportedConversion(format!("SWMR layout group '{}': {}", path, e))
879 })?;
880 created.insert(path.clone());
881 }
882 Ok(())
883 }
884
885 fn build_swmr_layout_hardlinks(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
895 let layout = match self.layout.as_ref() {
896 Some(l) => l,
897 None => return Ok(()),
898 };
899 fn collect<'a>(
900 g: &'a crate::hdf5_layout::LayoutGroup,
901 prefix: &str,
902 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
903 ) {
904 let here = if prefix.is_empty() {
905 g.name.clone()
906 } else {
907 format!("{}/{}", prefix, g.name)
908 };
909 for hl in &g.hardlinks {
910 out.push((here.clone(), hl));
911 }
912 for sub in &g.groups {
913 collect(sub, &here, out);
914 }
915 }
916 let mut links = Vec::new();
917 for g in &layout.groups {
918 collect(g, "", &mut links);
919 }
920 for (parent_path, hl) in &links {
921 let parent = format!("/{}", parent_path);
922 swmr.create_hard_link(&parent, &hl.name, &hl.target)
923 .map_err(|e| {
924 ADError::UnsupportedConversion(format!(
925 "SWMR layout hardlink '{}/{}' -> '{}': {}",
926 parent_path, hl.name, hl.target, e
927 ))
928 })?;
929 }
930 Ok(())
931 }
932
933 fn write_swmr_layout_dataset_attrs(
941 &self,
942 swmr: &mut SwmrFileWriter,
943 ds_index: usize,
944 ) -> ADResult<()> {
945 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
946 let layout = match self.layout.as_ref() {
947 Some(l) => l,
948 None => return Ok(()),
949 };
950 let resolved_ds = self.resolved_dataset_path.as_str();
951 let mut attrs: Vec<(String, LayoutDataType, String)> = Vec::new();
952 layout.for_each_dataset(|path, d| {
953 let full = format!("{}/{}", path, d.name);
954 if full.trim_start_matches('/') == resolved_ds {
955 for a in &d.attributes {
956 if a.source == LayoutSource::Constant {
957 attrs.push((a.name.clone(), a.data_type, a.value.clone()));
958 }
959 }
960 }
961 });
962 for (name, dtype, value) in &attrs {
963 match dtype {
964 LayoutDataType::Int => {
965 let v: i64 = value.trim().parse().unwrap_or(0);
966 swmr.set_dataset_attr_numeric(ds_index, name, &v)
967 }
968 LayoutDataType::Float => {
969 let v: f64 = value.trim().parse().unwrap_or(0.0);
970 swmr.set_dataset_attr_numeric(ds_index, name, &v)
971 }
972 LayoutDataType::String => swmr.set_dataset_attr_string(ds_index, name, value),
973 }
974 .map_err(|e| {
975 ADError::UnsupportedConversion(format!(
976 "SWMR layout dataset attribute '{}': {}",
977 name, e
978 ))
979 })?;
980 }
981 Ok(())
982 }
983
984 fn resolve_layout_paths(&mut self) {
996 let strip = |s: String| s.trim_start_matches('/').to_string();
997 match self.layout.as_ref() {
998 Some(layout) => {
999 self.resolved_dataset_path = layout
1000 .detector_dataset_path()
1001 .map(strip)
1002 .unwrap_or_else(|| self.dataset_name.clone());
1003 self.resolved_ndattr_group =
1004 layout.ndattr_default_group().map(strip).unwrap_or_default();
1005 self.resolved_perf_group = layout
1006 .dataset_group_path("timestamp")
1007 .map(strip)
1008 .unwrap_or_default();
1009 }
1010 None => {
1011 self.resolved_dataset_path = self.dataset_name.clone();
1012 self.resolved_ndattr_group.clear();
1013 self.resolved_perf_group.clear();
1014 }
1015 }
1016 }
1017
1018 fn build_layout_groups(&self, file: &H5File) -> ADResult<()> {
1027 let layout = match self.layout.as_ref() {
1028 Some(l) => l,
1029 None => return Ok(()),
1030 };
1031 fn collect(g: &crate::hdf5_layout::LayoutGroup, prefix: &str, out: &mut Vec<String>) {
1032 let here = if prefix.is_empty() {
1033 g.name.clone()
1034 } else {
1035 format!("{}/{}", prefix, g.name)
1036 };
1037 out.push(here.clone());
1038 for sub in &g.groups {
1039 collect(sub, &here, out);
1040 }
1041 }
1042 let mut paths = Vec::new();
1043 for g in &layout.groups {
1044 collect(g, "", &mut paths);
1045 }
1046 paths.sort_by_key(|p| p.matches('/').count());
1047 paths.dedup();
1048 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1049 for path in &paths {
1050 if created.contains(path) {
1051 continue;
1052 }
1053 let (parent, leaf) = match path.rsplit_once('/') {
1054 Some((p, l)) => (p, l),
1055 None => ("", path.as_str()),
1056 };
1057 let parent_group = if parent.is_empty() {
1059 None
1060 } else {
1061 Some(Self::open_write_group(file, parent)?)
1062 };
1063 match parent_group.as_ref() {
1064 Some(g) => g.create_group(leaf),
1065 None => file.create_group(leaf),
1066 }
1067 .map_err(|e| {
1068 ADError::UnsupportedConversion(format!("HDF5 layout group '{}': {}", path, e))
1069 })?;
1070 created.insert(path.clone());
1071 }
1072 Ok(())
1073 }
1074
1075 fn build_layout_hardlinks(&self, file: &H5File) -> ADResult<()> {
1093 let layout = match self.layout.as_ref() {
1094 Some(l) => l,
1095 None => return Ok(()),
1096 };
1097 fn collect<'a>(
1099 g: &'a crate::hdf5_layout::LayoutGroup,
1100 prefix: &str,
1101 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1102 ) {
1103 let here = if prefix.is_empty() {
1104 g.name.clone()
1105 } else {
1106 format!("{}/{}", prefix, g.name)
1107 };
1108 for hl in &g.hardlinks {
1109 out.push((here.clone(), hl));
1110 }
1111 for sub in &g.groups {
1112 collect(sub, &here, out);
1113 }
1114 }
1115 let mut links = Vec::new();
1116 for g in &layout.groups {
1117 collect(g, "", &mut links);
1118 }
1119 for (parent_path, hl) in &links {
1120 let parent = Self::open_write_group(file, parent_path)?;
1123 parent.link(&hl.name, &hl.target).map_err(|e| {
1124 ADError::UnsupportedConversion(format!(
1125 "HDF5 layout hardlink '{}/{}' -> '{}': {}",
1126 parent_path, hl.name, hl.target, e
1127 ))
1128 })?;
1129 }
1130 Ok(())
1131 }
1132
1133 fn open_write_group(file: &H5File, path: &str) -> ADResult<rust_hdf5::H5Group> {
1137 let mut current: Option<rust_hdf5::H5Group> = None;
1138 for seg in path.split('/').filter(|s| !s.is_empty()) {
1139 let next = match current.as_ref() {
1140 Some(g) => g.group(seg),
1141 None => file.root_group().group(seg),
1142 }
1143 .map_err(|e| {
1144 ADError::UnsupportedConversion(format!("HDF5 group reopen '{}': {}", seg, e))
1145 })?;
1146 current = Some(next);
1147 }
1148 current.ok_or_else(|| ADError::UnsupportedConversion("empty group path".into()))
1149 }
1150
1151 fn create_primary_dataset(&mut self, array: &NDArray) -> ADResult<()> {
1155 let frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1156 let (shape, chunk, extra_extent) = self.primary_layout(&frame_dims);
1157 let element_size = array.data.data_type().element_size();
1158 let pipeline = self.build_pipeline(element_size);
1159 let max_shape: Vec<Option<usize>> = shape
1166 .iter()
1167 .zip(chunk.iter())
1168 .enumerate()
1169 .map(|(i, (&s, &c))| {
1170 if i == 0 {
1171 if extra_extent.is_none() {
1172 None
1173 } else {
1174 Some(s)
1175 }
1176 } else if extra_extent.is_none() {
1177 Some(s.div_ceil(c) * c)
1178 } else {
1179 Some(s)
1180 }
1181 })
1182 .collect();
1183
1184 match self.handle {
1188 Some(Hdf5Handle::Standard { ref file, .. }) => self.build_layout_groups(file)?,
1189 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1190 }
1191
1192 let resolved_ds = self.resolved_dataset_path.clone();
1198 let layout_ds_attrs: Vec<(String, crate::hdf5_layout::LayoutDataType, String)> = self
1199 .layout
1200 .as_ref()
1201 .map(|l| {
1202 use crate::hdf5_layout::LayoutSource;
1203 let mut out = Vec::new();
1204 l.for_each_dataset(|path, d| {
1205 let full = format!("{}/{}", path, d.name);
1206 if full.trim_start_matches('/') == resolved_ds {
1207 for a in &d.attributes {
1208 if a.source == LayoutSource::Constant {
1209 out.push((a.name.clone(), a.data_type, a.value.clone()));
1210 }
1211 }
1212 }
1213 });
1214 out
1215 })
1216 .unwrap_or_default();
1217
1218 let h5file = match self.handle {
1219 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1220 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1221 };
1222
1223 let (ds_group, ds_name): (Option<rust_hdf5::H5Group>, String) =
1226 match self.resolved_dataset_path.rsplit_once('/') {
1227 Some((group_path, leaf)) => (
1228 Some(Self::open_write_group(h5file, group_path)?),
1229 leaf.to_string(),
1230 ),
1231 None => (None, self.resolved_dataset_path.clone()),
1232 };
1233
1234 let dtype_ordinal = array.data.data_type() as i32;
1235 let fill = self.fill_value;
1236 let row_chunks = self.chunk.n_row_chunks as i32;
1237 let col_chunks = self.chunk.n_col_chunks as i32;
1238 let frame_chunks = self.chunk.n_frames_chunks as i32;
1239 let n_extra = self.n_extra_dims as i32;
1240 let extra_meta: Vec<(usize, i32, String)> = (0..self.n_extra_dims)
1241 .map(|i| {
1242 (
1243 i,
1244 self.extra_dims[i].size.max(1) as i32,
1245 self.extra_dims[i].name.clone(),
1246 )
1247 })
1248 .collect();
1249
1250 macro_rules! create_ds {
1251 ($t:ty) => {{
1252 let mut builder = match ds_group.as_ref() {
1253 Some(g) => g.new_dataset::<$t>(),
1254 None => h5file.new_dataset::<$t>(),
1255 }
1256 .shape(&shape[..])
1257 .chunk(&chunk[..])
1258 .max_shape(&max_shape[..])
1259 .fill_value(fill as $t);
1265 if let Some(ref pl) = pipeline {
1266 builder = builder.filter_pipeline(pl.clone());
1267 }
1268 let ds = builder.create(ds_name.as_str()).map_err(|e| {
1269 ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e))
1270 })?;
1271 let _ = ds
1273 .new_attr::<i32>()
1274 .shape(())
1275 .create(DTYPE_ATTR)
1276 .and_then(|a| a.write_numeric(&dtype_ordinal));
1277 let _ = ds
1281 .new_attr::<f64>()
1282 .shape(())
1283 .create("HDF5_fillValue")
1284 .and_then(|a| a.write_numeric(&fill));
1285 for (name, val) in [
1289 ("HDF5_nRowChunks", row_chunks),
1290 ("HDF5_nColChunks", col_chunks),
1291 ("HDF5_nFramesChunks", frame_chunks),
1292 ("HDF5_nExtraDims", n_extra),
1293 ] {
1294 let _ = ds
1295 .new_attr::<i32>()
1296 .shape(())
1297 .create(name)
1298 .and_then(|a| a.write_numeric(&val));
1299 }
1300 for (i, size, name) in &extra_meta {
1303 let _ = ds
1304 .new_attr::<i32>()
1305 .shape(())
1306 .create(&format!("HDF5_extraDimSize{}", i))
1307 .and_then(|a| a.write_numeric(size));
1308 if !name.is_empty() {
1309 let s = rust_hdf5::types::VarLenUnicode(name.clone());
1310 let _ = ds
1311 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1312 .shape(())
1313 .create(&format!("HDF5_extraDimName{}", i))
1314 .and_then(|a| a.write_scalar(&s));
1315 }
1316 }
1317 for (aname, atype, avalue) in &layout_ds_attrs {
1320 use crate::hdf5_layout::LayoutDataType;
1321 match atype {
1322 LayoutDataType::Int => {
1323 let v: i64 = avalue.trim().parse().unwrap_or(0);
1324 let _ = ds
1325 .new_attr::<i64>()
1326 .shape(())
1327 .create(aname)
1328 .and_then(|a| a.write_numeric(&v));
1329 }
1330 LayoutDataType::Float => {
1331 let v: f64 = avalue.trim().parse().unwrap_or(0.0);
1332 let _ = ds
1333 .new_attr::<f64>()
1334 .shape(())
1335 .create(aname)
1336 .and_then(|a| a.write_numeric(&v));
1337 }
1338 LayoutDataType::String => {
1339 let s = rust_hdf5::types::VarLenUnicode(avalue.clone());
1340 let _ = ds
1341 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1342 .shape(())
1343 .create(aname)
1344 .and_then(|a| a.write_scalar(&s));
1345 }
1346 }
1347 }
1348 ds
1349 }};
1350 }
1351
1352 let ds = match array.data {
1353 NDDataBuffer::I8(_) => create_ds!(i8),
1354 NDDataBuffer::U8(_) => create_ds!(u8),
1355 NDDataBuffer::I16(_) => create_ds!(i16),
1356 NDDataBuffer::U16(_) => create_ds!(u16),
1357 NDDataBuffer::I32(_) => create_ds!(i32),
1358 NDDataBuffer::U32(_) => create_ds!(u32),
1359 NDDataBuffer::I64(_) => create_ds!(i64),
1360 NDDataBuffer::U64(_) => create_ds!(u64),
1361 NDDataBuffer::F32(_) => create_ds!(f32),
1362 NDDataBuffer::F64(_) => create_ds!(f64),
1363 };
1364
1365 if let Some(Hdf5Handle::Standard { primary, .. }) = self.handle.as_mut() {
1366 *primary = Some(ds);
1367 }
1368 self.open_data_type = Some(array.data.data_type());
1369 self.open_frame_dims = Some(frame_dims);
1370 self.open_extra_extent = extra_extent;
1371 Ok(())
1372 }
1373
1374 fn write_standard(&mut self, array: &NDArray) -> ADResult<()> {
1377 if self.frame_count == 0 {
1378 self.create_primary_dataset(array)?;
1379 self.create_attribute_datasets(array);
1380 }
1381
1382 let frame_dims = self
1383 .open_frame_dims
1384 .clone()
1385 .ok_or_else(|| ADError::UnsupportedConversion("dataset not initialised".into()))?;
1386 let cur_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1387 if cur_dims != frame_dims {
1388 return Err(ADError::UnsupportedConversion(format!(
1389 "HDF5 frame shape changed mid-stream: {:?} != {:?}",
1390 cur_dims, frame_dims
1391 )));
1392 }
1393
1394 let (_shape, chunk, _extra) = self.primary_layout(&frame_dims);
1395 let frame_idx = self.frame_count;
1396 let extra_extent = self.open_extra_extent;
1397 let elem_size = array.data.data_type().element_size();
1398 let fc = chunk[0];
1399
1400 if let Some(total) = extra_extent {
1403 if frame_idx >= total {
1404 return Err(ADError::UnsupportedConversion(format!(
1405 "HDF5 extra-dimension capacity exceeded: frame {} >= {}",
1406 frame_idx, total
1407 )));
1408 }
1409 }
1410
1411 self.frame_band.push(nd_buffer_to_le_bytes(&array.data));
1417 if self.frame_band.len() >= fc {
1418 let band_idx = frame_idx / fc;
1419 let ds = match self.handle {
1420 Some(Hdf5Handle::Standard {
1421 primary: Some(ref ds),
1422 ..
1423 }) => ds,
1424 _ => {
1425 return Err(ADError::UnsupportedConversion(
1426 "HDF5 primary dataset not initialised".into(),
1427 ));
1428 }
1429 };
1430 Self::flush_band(
1431 ds,
1432 band_idx,
1433 &self.frame_band,
1434 &frame_dims,
1435 &chunk,
1436 elem_size,
1437 )?;
1438 self.frame_band.clear();
1439 }
1440
1441 if self.store_attributes {
1443 for ad in self.attr_datasets.iter_mut() {
1444 let value = array
1445 .attributes
1446 .get(&ad.name)
1447 .map(|a| a.value.clone())
1448 .unwrap_or(NDAttrValue::Undefined);
1449 ad.push(&value);
1450 }
1451 }
1452 Ok(())
1453 }
1454
1455 fn create_attribute_datasets(&mut self, array: &NDArray) {
1458 self.attr_datasets.clear();
1459 if !self.store_attributes {
1460 return;
1461 }
1462 for attr in array.attributes.iter() {
1463 let dt = attr.value.data_type();
1464 self.attr_datasets
1465 .push(AttributeDataset::new(attr.name.clone(), dt));
1466 }
1467 }
1468
1469 fn flush_attribute_datasets(&mut self) -> ADResult<()> {
1472 if self.attr_datasets.is_empty() {
1473 return Ok(());
1474 }
1475 let chunk_depth = self.chunk.ndattr_chunk.max(1);
1476 let ndattr_group = self.resolved_ndattr_group.clone();
1477 let h5file = match self.handle {
1478 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1479 _ => return Ok(()),
1480 };
1481 let group = if ndattr_group.is_empty() {
1485 h5file
1486 .create_group("NDAttributes")
1487 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
1488 } else {
1489 Self::open_write_group(h5file, &ndattr_group)?
1490 };
1491
1492 for ad in self.attr_datasets.iter() {
1493 if ad.frames == 0 {
1494 continue;
1495 }
1496 let n = ad.frames;
1497 let chunk = chunk_depth.min(n).max(1);
1498
1499 macro_rules! write_attr_ds {
1500 ($t:ty) => {{
1501 let es = std::mem::size_of::<$t>();
1502 let ds = group
1503 .new_dataset::<$t>()
1504 .shape(&[n])
1505 .chunk(&[chunk])
1506 .max_shape(&[None])
1507 .create(&ad.name)
1508 .map_err(|e| {
1509 ADError::UnsupportedConversion(format!(
1510 "HDF5 attribute dataset error: {}",
1511 e
1512 ))
1513 })?;
1514 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
1518 }};
1519 }
1520
1521 match ad.data_type {
1522 NDAttrDataType::Int8 => write_attr_ds!(i8),
1523 NDAttrDataType::UInt8 => write_attr_ds!(u8),
1524 NDAttrDataType::Int16 => write_attr_ds!(i16),
1525 NDAttrDataType::UInt16 => write_attr_ds!(u16),
1526 NDAttrDataType::Int32 => write_attr_ds!(i32),
1527 NDAttrDataType::UInt32 => write_attr_ds!(u32),
1528 NDAttrDataType::Int64 => write_attr_ds!(i64),
1529 NDAttrDataType::UInt64 => write_attr_ds!(u64),
1530 NDAttrDataType::Float32 => write_attr_ds!(f32),
1531 NDAttrDataType::Float64 => write_attr_ds!(f64),
1532 NDAttrDataType::String => {
1533 let es = MAX_ATTRIBUTE_STRING_SIZE;
1535 let ds = group
1536 .new_dataset::<u8>()
1537 .shape([n, es])
1538 .chunk(&[chunk, es])
1539 .max_shape(&[None, Some(es)])
1540 .create(&ad.name)
1541 .map_err(|e| {
1542 ADError::UnsupportedConversion(format!(
1543 "HDF5 attribute dataset error: {}",
1544 e
1545 ))
1546 })?;
1547 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
1548 }
1549 }
1550 }
1551 Ok(())
1552 }
1553
1554 fn flush_performance_dataset(&mut self) -> ADResult<()> {
1557 if !self.store_performance || self.perf_rows.is_empty() {
1558 return Ok(());
1559 }
1560 let n = self.perf_rows.len();
1561 let mut flat: Vec<f64> = Vec::with_capacity(n * 5);
1562 for row in &self.perf_rows {
1563 flat.extend_from_slice(row);
1564 }
1565 let raw: Vec<u8> = flat.iter().flat_map(|v| v.to_le_bytes()).collect();
1568
1569 let perf_group = self.resolved_perf_group.clone();
1570 let h5file = match self.handle {
1571 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1572 _ => return Ok(()),
1573 };
1574 let group = if perf_group.is_empty() {
1579 h5file
1580 .create_group("performance")
1581 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
1582 } else {
1583 Self::open_write_group(h5file, &perf_group)?
1584 };
1585 let ds = group
1586 .new_dataset::<f64>()
1587 .shape([n, 5])
1588 .chunk(&[1, 5])
1589 .max_shape(&[None, Some(5)])
1590 .create("timestamp")
1591 .map_err(|e| {
1592 ADError::UnsupportedConversion(format!("HDF5 performance dataset error: {}", e))
1593 })?;
1594 for f in 0..n {
1595 let start = f * 5 * 8;
1596 let end = start + 5 * 8;
1597 ds.write_chunk(f, &raw[start..end]).map_err(|e| {
1598 ADError::UnsupportedConversion(format!("HDF5 performance write error: {}", e))
1599 })?;
1600 }
1601 Ok(())
1602 }
1603
1604 fn write_swmr(&mut self, array: &NDArray) -> ADResult<()> {
1606 let (writer, ds_index) = match self.handle {
1607 Some(Hdf5Handle::Swmr {
1608 ref mut writer,
1609 ds_index,
1610 ..
1611 }) => (writer, ds_index),
1612 _ => return Err(ADError::UnsupportedConversion("no SWMR writer open".into())),
1613 };
1614
1615 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
1619 writer
1620 .append_frame(ds_index, &frame_bytes)
1621 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR append error: {}", e)))?;
1622
1623 let count = self.frame_count + 1; if self.flush_nth_frame > 0 && count % self.flush_nth_frame == 0 {
1626 writer
1627 .flush()
1628 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR flush error: {}", e)))?;
1629 }
1630 Ok(())
1631 }
1632
1633 fn record_performance(&mut self, write_duration: f64, frame_bytes: usize) {
1635 let now = std::time::Instant::now();
1636 let first = *self.perf_first.get_or_insert(now);
1637 let runtime = now.duration_since(first).as_secs_f64();
1638 let period = match self.perf_prev {
1639 Some(prev) => now.duration_since(prev).as_secs_f64(),
1640 None => write_duration,
1641 };
1642 self.perf_prev = Some(now);
1643 let fb = frame_bytes as f64;
1644 let inst_speed = if period > 0.0 { fb / period } else { 0.0 };
1645 let avg_speed = if runtime > 0.0 {
1646 (self.perf_rows.len() as f64 + 1.0) * fb / runtime
1647 } else {
1648 0.0
1649 };
1650 self.perf_rows
1651 .push([write_duration, period, runtime, inst_speed, avg_speed]);
1652 }
1653}
1654
1655fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
1667 match buf {
1668 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
1669 NDDataBuffer::U8(v) => v.clone(),
1670 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1671 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1672 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1673 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1674 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1675 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1676 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1677 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
1678 }
1679}
1680
1681fn write_chunked_buffer(
1685 ds: &rust_hdf5::H5Dataset,
1686 buffer: &[u8],
1687 chunk_bytes: usize,
1688) -> ADResult<()> {
1689 let n_chunks = buffer.len().div_ceil(chunk_bytes.max(1));
1690 for c in 0..n_chunks {
1691 let start = c * chunk_bytes;
1692 let end = ((c + 1) * chunk_bytes).min(buffer.len());
1693 let slice = &buffer[start..end];
1694 if slice.len() == chunk_bytes {
1695 ds.write_chunk(c, slice)
1696 } else {
1697 let mut padded = vec![0u8; chunk_bytes];
1698 padded[..slice.len()].copy_from_slice(slice);
1699 ds.write_chunk(c, &padded)
1700 }
1701 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 chunk write error: {}", e)))?;
1702 }
1703 Ok(())
1704}
1705
1706impl Default for Hdf5Writer {
1707 fn default() -> Self {
1708 Self::new()
1709 }
1710}
1711
1712impl NDFileWriter for Hdf5Writer {
1713 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
1714 self.current_path = Some(path.to_path_buf());
1715 self.frame_count = 0;
1716 self.frame_band.clear();
1717 self.total_runtime = 0.0;
1718 self.total_bytes = 0;
1719 self.swmr_cb_counter = 0;
1720 self.open_data_type = None;
1721 self.open_frame_dims = None;
1722 self.open_extra_extent = None;
1723 self.perf_rows.clear();
1724 self.perf_prev = None;
1725 self.perf_first = None;
1726 self.attr_datasets.clear();
1727 self.resolve_layout_paths();
1730
1731 if self.swmr_mode && mode == NDFileMode::Stream {
1732 self.open_swmr(path, array)
1733 } else {
1734 let h5file = H5File::create(path)
1735 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 create error: {}", e)))?;
1736 self.handle = Some(Hdf5Handle::Standard {
1737 file: h5file,
1738 primary: None,
1739 });
1740 Ok(())
1741 }
1742 }
1743
1744 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
1745 let start = std::time::Instant::now();
1746
1747 let is_swmr = matches!(self.handle, Some(Hdf5Handle::Swmr { .. }));
1748 if is_swmr {
1749 self.write_swmr(array)?;
1750 } else {
1751 self.write_standard(array)?;
1752 }
1753 self.frame_count += 1;
1754
1755 let elapsed = start.elapsed().as_secs_f64();
1756 let frame_bytes = array.data.as_u8_slice().len();
1757 if self.store_performance {
1758 self.total_runtime += elapsed;
1759 self.total_bytes += frame_bytes as u64;
1760 self.record_performance(elapsed, frame_bytes);
1761 }
1762 Ok(())
1763 }
1764
1765 fn read_file(&mut self) -> ADResult<NDArray> {
1766 self.resolve_layout_paths();
1770 let dataset_path = self.resolved_dataset_path.clone();
1771 let path = self
1772 .current_path
1773 .as_ref()
1774 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
1775
1776 let h5file = H5File::open(path)
1777 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 open error: {}", e)))?;
1778
1779 let ds = h5file
1780 .dataset(&dataset_path)
1781 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e)))?;
1782
1783 let shape = ds.shape();
1784 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
1785 let element_size = ds.element_size();
1786
1787 let recorded: Option<NDDataType> = ds
1789 .attr(DTYPE_ATTR)
1790 .ok()
1791 .and_then(|a| a.read_numeric::<i32>().ok())
1792 .and_then(|v| NDDataType::from_ordinal(v as u8));
1793
1794 let data_type = recorded.unwrap_or(match element_size {
1795 1 => NDDataType::UInt8,
1796 2 => NDDataType::UInt16,
1797 4 => NDDataType::Float32,
1798 8 => NDDataType::Float64,
1799 other => {
1800 return Err(ADError::UnsupportedConversion(format!(
1801 "unsupported HDF5 element size {}",
1802 other
1803 )));
1804 }
1805 });
1806
1807 macro_rules! read_typed {
1808 ($t:ty, $variant:ident) => {{
1809 let data = ds.read_raw::<$t>().map_err(|e| {
1810 ADError::UnsupportedConversion(format!("HDF5 read error: {}", e))
1811 })?;
1812 let mut arr = NDArray::new(dims, data_type);
1813 arr.data = NDDataBuffer::$variant(data);
1814 return Ok(arr);
1815 }};
1816 }
1817
1818 match data_type {
1819 NDDataType::Int8 => read_typed!(i8, I8),
1820 NDDataType::UInt8 => read_typed!(u8, U8),
1821 NDDataType::Int16 => read_typed!(i16, I16),
1822 NDDataType::UInt16 => read_typed!(u16, U16),
1823 NDDataType::Int32 => read_typed!(i32, I32),
1824 NDDataType::UInt32 => read_typed!(u32, U32),
1825 NDDataType::Int64 => read_typed!(i64, I64),
1826 NDDataType::UInt64 => read_typed!(u64, U64),
1827 NDDataType::Float32 => read_typed!(f32, F32),
1828 NDDataType::Float64 => read_typed!(f64, F64),
1829 }
1830 }
1831
1832 fn close_file(&mut self) -> ADResult<()> {
1833 match self.handle {
1834 Some(Hdf5Handle::Standard { .. }) => {
1835 self.finalize_standard_primary()?;
1839 self.flush_attribute_datasets()?;
1840 self.flush_performance_dataset()?;
1841 match self.handle {
1844 Some(Hdf5Handle::Standard { ref file, .. }) => {
1845 self.build_layout_hardlinks(file)?
1846 }
1847 _ => unreachable!("handle is Standard in this arm"),
1848 }
1849 self.handle = None;
1850 }
1851 Some(Hdf5Handle::Swmr { .. }) => {
1852 if let Some(Hdf5Handle::Swmr { writer, .. }) = self.handle.take() {
1859 writer.close().map_err(|e| {
1860 ADError::UnsupportedConversion(format!("SWMR close error: {}", e))
1861 })?;
1862 }
1863 }
1864 None => {}
1865 }
1866 self.current_path = None;
1867 Ok(())
1868 }
1869
1870 fn supports_multiple_arrays(&self) -> bool {
1871 true
1872 }
1873}
1874
1875#[derive(Default)]
1881struct Hdf5ParamIndices {
1882 compression_type: Option<usize>,
1883 z_compress_level: Option<usize>,
1884 szip_num_pixels: Option<usize>,
1885 nbit_precision: Option<usize>,
1886 nbit_offset: Option<usize>,
1887 jpeg_quality: Option<usize>,
1888 blosc_shuffle_type: Option<usize>,
1889 blosc_compressor: Option<usize>,
1890 blosc_compress_level: Option<usize>,
1891 store_attributes: Option<usize>,
1892 store_performance: Option<usize>,
1893 total_runtime: Option<usize>,
1894 total_io_speed: Option<usize>,
1895 swmr_mode: Option<usize>,
1896 swmr_flush_now: Option<usize>,
1897 swmr_running: Option<usize>,
1898 swmr_cb_counter: Option<usize>,
1899 swmr_supported: Option<usize>,
1900 flush_nth_frame: Option<usize>,
1901 chunk_size_auto: Option<usize>,
1902 n_row_chunks: Option<usize>,
1903 n_col_chunks: Option<usize>,
1904 n_frames_chunks: Option<usize>,
1905 ndattr_chunk: Option<usize>,
1906 n_extra_dims: Option<usize>,
1907 extra_dim_size: [Option<usize>; MAX_EXTRA_DIMS],
1908 extra_dim_name: [Option<usize>; MAX_EXTRA_DIMS],
1909 fill_value: Option<usize>,
1910 dim_att_datasets: Option<usize>,
1911 layout_filename: Option<usize>,
1912 layout_valid: Option<usize>,
1913 layout_error_msg: Option<usize>,
1914}
1915
1916pub struct Hdf5FileProcessor {
1918 ctrl: FilePluginController<Hdf5Writer>,
1919 hdf5_params: Hdf5ParamIndices,
1920}
1921
1922impl Hdf5FileProcessor {
1923 pub fn new() -> Self {
1924 Self {
1925 ctrl: FilePluginController::new(Hdf5Writer::new()),
1926 hdf5_params: Hdf5ParamIndices::default(),
1927 }
1928 }
1929
1930 pub fn set_dataset_name(&mut self, name: &str) {
1931 self.ctrl.writer.set_dataset_name(name);
1932 }
1933}
1934
1935fn register_hdf5_params(
1937 base: &mut asyn_rs::port::PortDriverBase,
1938) -> asyn_rs::error::AsynResult<()> {
1939 use asyn_rs::param::ParamType;
1940 base.create_param("HDF5_SWMRFlushNow", ParamType::Int32)?;
1941 base.create_param("HDF5_chunkSizeAuto", ParamType::Int32)?;
1942 base.create_param("HDF5_nRowChunks", ParamType::Int32)?;
1943 base.create_param("HDF5_nColChunks", ParamType::Int32)?;
1944 base.create_param("HDF5_chunkSize2", ParamType::Int32)?;
1945 base.create_param("HDF5_chunkSize3", ParamType::Int32)?;
1946 base.create_param("HDF5_chunkSize4", ParamType::Int32)?;
1947 base.create_param("HDF5_chunkSize5", ParamType::Int32)?;
1948 base.create_param("HDF5_chunkSize6", ParamType::Int32)?;
1949 base.create_param("HDF5_chunkSize7", ParamType::Int32)?;
1950 base.create_param("HDF5_chunkSize8", ParamType::Int32)?;
1951 base.create_param("HDF5_chunkSize9", ParamType::Int32)?;
1952 base.create_param("HDF5_nFramesChunks", ParamType::Int32)?;
1953 base.create_param("HDF5_NDAttributeChunk", ParamType::Int32)?;
1954 base.create_param("HDF5_chunkBoundaryAlign", ParamType::Int32)?;
1955 base.create_param("HDF5_chunkBoundaryThreshold", ParamType::Int32)?;
1956 base.create_param("HDF5_nExtraDims", ParamType::Int32)?;
1957 base.create_param("HDF5_extraDimSizeN", ParamType::Int32)?;
1958 base.create_param("HDF5_extraDimNameN", ParamType::Octet)?;
1959 base.create_param("HDF5_extraDimSizeX", ParamType::Int32)?;
1960 base.create_param("HDF5_extraDimNameX", ParamType::Octet)?;
1961 base.create_param("HDF5_extraDimSizeY", ParamType::Int32)?;
1962 base.create_param("HDF5_extraDimNameY", ParamType::Octet)?;
1963 base.create_param("HDF5_extraDimSize3", ParamType::Int32)?;
1964 base.create_param("HDF5_extraDimName3", ParamType::Octet)?;
1965 base.create_param("HDF5_extraDimSize4", ParamType::Int32)?;
1966 base.create_param("HDF5_extraDimName4", ParamType::Octet)?;
1967 base.create_param("HDF5_extraDimSize5", ParamType::Int32)?;
1968 base.create_param("HDF5_extraDimName5", ParamType::Octet)?;
1969 base.create_param("HDF5_extraDimSize6", ParamType::Int32)?;
1970 base.create_param("HDF5_extraDimName6", ParamType::Octet)?;
1971 base.create_param("HDF5_extraDimSize7", ParamType::Int32)?;
1972 base.create_param("HDF5_extraDimName7", ParamType::Octet)?;
1973 base.create_param("HDF5_extraDimSize8", ParamType::Int32)?;
1974 base.create_param("HDF5_extraDimName8", ParamType::Octet)?;
1975 base.create_param("HDF5_extraDimSize9", ParamType::Int32)?;
1976 base.create_param("HDF5_extraDimName9", ParamType::Octet)?;
1977 base.create_param("HDF5_storeAttributes", ParamType::Int32)?;
1978 base.create_param("HDF5_storePerformance", ParamType::Int32)?;
1979 base.create_param("HDF5_totalRuntime", ParamType::Float64)?;
1980 base.create_param("HDF5_totalIoSpeed", ParamType::Float64)?;
1981 base.create_param("HDF5_flushNthFrame", ParamType::Int32)?;
1982 base.create_param("HDF5_compressionType", ParamType::Int32)?;
1983 base.create_param("HDF5_nbitsPrecision", ParamType::Int32)?;
1984 base.create_param("HDF5_nbitsOffset", ParamType::Int32)?;
1985 base.create_param("HDF5_szipNumPixels", ParamType::Int32)?;
1986 base.create_param("HDF5_zCompressLevel", ParamType::Int32)?;
1987 base.create_param("HDF5_bloscShuffleType", ParamType::Int32)?;
1988 base.create_param("HDF5_bloscCompressor", ParamType::Int32)?;
1989 base.create_param("HDF5_bloscCompressLevel", ParamType::Int32)?;
1990 base.create_param("HDF5_jpegQuality", ParamType::Int32)?;
1991 base.create_param("HDF5_dimAttDatasets", ParamType::Int32)?;
1992 base.create_param("HDF5_layoutErrorMsg", ParamType::Octet)?;
1993 base.create_param("HDF5_layoutValid", ParamType::Int32)?;
1994 base.create_param("HDF5_layoutFilename", ParamType::Octet)?;
1995 base.create_param("HDF5_SWMRSupported", ParamType::Int32)?;
1996 base.create_param("HDF5_SWMRMode", ParamType::Int32)?;
1997 base.create_param("HDF5_SWMRRunning", ParamType::Int32)?;
1998 base.create_param("HDF5_SWMRCbCounter", ParamType::Int32)?;
1999 base.create_param("HDF5_posRunning", ParamType::Int32)?;
2000 base.create_param("HDF5_posNameDimN", ParamType::Octet)?;
2001 base.create_param("HDF5_posNameDimX", ParamType::Octet)?;
2002 base.create_param("HDF5_posNameDimY", ParamType::Octet)?;
2003 base.create_param("HDF5_posNameDim3", ParamType::Octet)?;
2004 base.create_param("HDF5_posNameDim4", ParamType::Octet)?;
2005 base.create_param("HDF5_posNameDim5", ParamType::Octet)?;
2006 base.create_param("HDF5_posNameDim6", ParamType::Octet)?;
2007 base.create_param("HDF5_posNameDim7", ParamType::Octet)?;
2008 base.create_param("HDF5_posNameDim8", ParamType::Octet)?;
2009 base.create_param("HDF5_posNameDim9", ParamType::Octet)?;
2010 base.create_param("HDF5_posIndexDimN", ParamType::Octet)?;
2011 base.create_param("HDF5_posIndexDimX", ParamType::Octet)?;
2012 base.create_param("HDF5_posIndexDimY", ParamType::Octet)?;
2013 base.create_param("HDF5_posIndexDim3", ParamType::Octet)?;
2014 base.create_param("HDF5_posIndexDim4", ParamType::Octet)?;
2015 base.create_param("HDF5_posIndexDim5", ParamType::Octet)?;
2016 base.create_param("HDF5_posIndexDim6", ParamType::Octet)?;
2017 base.create_param("HDF5_posIndexDim7", ParamType::Octet)?;
2018 base.create_param("HDF5_posIndexDim8", ParamType::Octet)?;
2019 base.create_param("HDF5_posIndexDim9", ParamType::Octet)?;
2020 base.create_param("HDF5_fillValue", ParamType::Float64)?;
2021 base.create_param("HDF5_extraDimChunkX", ParamType::Int32)?;
2022 base.create_param("HDF5_extraDimChunkY", ParamType::Int32)?;
2023 base.create_param("HDF5_extraDimChunk3", ParamType::Int32)?;
2024 base.create_param("HDF5_extraDimChunk4", ParamType::Int32)?;
2025 base.create_param("HDF5_extraDimChunk5", ParamType::Int32)?;
2026 base.create_param("HDF5_extraDimChunk6", ParamType::Int32)?;
2027 base.create_param("HDF5_extraDimChunk7", ParamType::Int32)?;
2028 base.create_param("HDF5_extraDimChunk8", ParamType::Int32)?;
2029 base.create_param("HDF5_extraDimChunk9", ParamType::Int32)?;
2030 Ok(())
2031}
2032
2033impl Default for Hdf5FileProcessor {
2034 fn default() -> Self {
2035 Self::new()
2036 }
2037}
2038
2039const EXTRA_DIM_SIZE_PARAMS: [&str; MAX_EXTRA_DIMS] = [
2041 "HDF5_extraDimSizeN",
2042 "HDF5_extraDimSizeX",
2043 "HDF5_extraDimSizeY",
2044 "HDF5_extraDimSize3",
2045 "HDF5_extraDimSize4",
2046 "HDF5_extraDimSize5",
2047 "HDF5_extraDimSize6",
2048 "HDF5_extraDimSize7",
2049 "HDF5_extraDimSize8",
2050 "HDF5_extraDimSize9",
2051];
2052
2053const EXTRA_DIM_NAME_PARAMS: [&str; MAX_EXTRA_DIMS] = [
2055 "HDF5_extraDimNameN",
2056 "HDF5_extraDimNameX",
2057 "HDF5_extraDimNameY",
2058 "HDF5_extraDimName3",
2059 "HDF5_extraDimName4",
2060 "HDF5_extraDimName5",
2061 "HDF5_extraDimName6",
2062 "HDF5_extraDimName7",
2063 "HDF5_extraDimName8",
2064 "HDF5_extraDimName9",
2065];
2066
2067impl NDPluginProcess for Hdf5FileProcessor {
2068 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2069 let was_swmr = self.ctrl.writer.is_swmr_active();
2070 let mut result = self.ctrl.process_array(array);
2071 let is_swmr = self.ctrl.writer.is_swmr_active();
2072
2073 if was_swmr != is_swmr {
2075 if let Some(idx) = self.hdf5_params.swmr_running {
2076 result
2077 .param_updates
2078 .push(ParamUpdate::int32(idx, if is_swmr { 1 } else { 0 }));
2079 }
2080 }
2081
2082 if is_swmr {
2084 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
2085 result.param_updates.push(ParamUpdate::int32(
2086 idx,
2087 self.ctrl.writer.swmr_cb_counter as i32,
2088 ));
2089 }
2090 }
2091
2092 if self.ctrl.writer.store_performance {
2094 if let Some(idx) = self.hdf5_params.total_runtime {
2095 result
2096 .param_updates
2097 .push(ParamUpdate::float64(idx, self.ctrl.writer.total_runtime));
2098 }
2099 if let Some(idx) = self.hdf5_params.total_io_speed {
2100 let speed = if self.ctrl.writer.total_runtime > 0.0 {
2101 self.ctrl.writer.total_bytes as f64
2102 / self.ctrl.writer.total_runtime
2103 / 1_000_000.0
2104 } else {
2105 0.0
2106 };
2107 result.param_updates.push(ParamUpdate::float64(idx, speed));
2108 }
2109 }
2110
2111 result
2112 }
2113
2114 fn plugin_type(&self) -> &str {
2115 "NDFileHDF5"
2116 }
2117
2118 fn register_params(
2119 &mut self,
2120 base: &mut asyn_rs::port::PortDriverBase,
2121 ) -> asyn_rs::error::AsynResult<()> {
2122 self.ctrl.register_params(base)?;
2123 register_hdf5_params(base)?;
2124 self.hdf5_params.compression_type = base.find_param("HDF5_compressionType");
2125 self.hdf5_params.z_compress_level = base.find_param("HDF5_zCompressLevel");
2126 self.hdf5_params.szip_num_pixels = base.find_param("HDF5_szipNumPixels");
2127 self.hdf5_params.nbit_precision = base.find_param("HDF5_nbitsPrecision");
2128 self.hdf5_params.nbit_offset = base.find_param("HDF5_nbitsOffset");
2129 self.hdf5_params.jpeg_quality = base.find_param("HDF5_jpegQuality");
2130 self.hdf5_params.blosc_shuffle_type = base.find_param("HDF5_bloscShuffleType");
2131 self.hdf5_params.blosc_compressor = base.find_param("HDF5_bloscCompressor");
2132 self.hdf5_params.blosc_compress_level = base.find_param("HDF5_bloscCompressLevel");
2133 self.hdf5_params.store_attributes = base.find_param("HDF5_storeAttributes");
2134 self.hdf5_params.store_performance = base.find_param("HDF5_storePerformance");
2135 self.hdf5_params.total_runtime = base.find_param("HDF5_totalRuntime");
2136 self.hdf5_params.total_io_speed = base.find_param("HDF5_totalIoSpeed");
2137 self.hdf5_params.swmr_mode = base.find_param("HDF5_SWMRMode");
2138 self.hdf5_params.swmr_flush_now = base.find_param("HDF5_SWMRFlushNow");
2139 self.hdf5_params.swmr_running = base.find_param("HDF5_SWMRRunning");
2140 self.hdf5_params.swmr_cb_counter = base.find_param("HDF5_SWMRCbCounter");
2141 self.hdf5_params.swmr_supported = base.find_param("HDF5_SWMRSupported");
2142 self.hdf5_params.flush_nth_frame = base.find_param("HDF5_flushNthFrame");
2143 self.hdf5_params.chunk_size_auto = base.find_param("HDF5_chunkSizeAuto");
2144 self.hdf5_params.n_row_chunks = base.find_param("HDF5_nRowChunks");
2145 self.hdf5_params.n_col_chunks = base.find_param("HDF5_nColChunks");
2146 self.hdf5_params.n_frames_chunks = base.find_param("HDF5_nFramesChunks");
2147 self.hdf5_params.ndattr_chunk = base.find_param("HDF5_NDAttributeChunk");
2148 self.hdf5_params.n_extra_dims = base.find_param("HDF5_nExtraDims");
2149 for i in 0..MAX_EXTRA_DIMS {
2150 self.hdf5_params.extra_dim_size[i] = base.find_param(EXTRA_DIM_SIZE_PARAMS[i]);
2151 self.hdf5_params.extra_dim_name[i] = base.find_param(EXTRA_DIM_NAME_PARAMS[i]);
2152 }
2153 self.hdf5_params.fill_value = base.find_param("HDF5_fillValue");
2154 self.hdf5_params.dim_att_datasets = base.find_param("HDF5_dimAttDatasets");
2155 self.hdf5_params.layout_filename = base.find_param("HDF5_layoutFilename");
2156 self.hdf5_params.layout_valid = base.find_param("HDF5_layoutValid");
2157 self.hdf5_params.layout_error_msg = base.find_param("HDF5_layoutErrorMsg");
2158
2159 if let Some(idx) = self.hdf5_params.swmr_supported {
2161 base.set_int32_param(idx, 0, 1)?;
2162 }
2163 Ok(())
2164 }
2165
2166 fn on_param_change(
2167 &mut self,
2168 reason: usize,
2169 params: &PluginParamSnapshot,
2170 ) -> ParamChangeResult {
2171 if Some(reason) == self.hdf5_params.compression_type {
2173 self.ctrl.writer.set_compression_type(params.value.as_i32());
2174 return ParamChangeResult::updates(vec![]);
2175 }
2176 if Some(reason) == self.hdf5_params.z_compress_level {
2177 self.ctrl
2178 .writer
2179 .set_z_compress_level(params.value.as_i32() as u32);
2180 return ParamChangeResult::updates(vec![]);
2181 }
2182 if Some(reason) == self.hdf5_params.szip_num_pixels {
2183 self.ctrl
2184 .writer
2185 .set_szip_num_pixels(params.value.as_i32() as u32);
2186 return ParamChangeResult::updates(vec![]);
2187 }
2188 if Some(reason) == self.hdf5_params.blosc_shuffle_type {
2189 self.ctrl
2190 .writer
2191 .set_blosc_shuffle_type(params.value.as_i32());
2192 return ParamChangeResult::updates(vec![]);
2193 }
2194 if Some(reason) == self.hdf5_params.blosc_compressor {
2195 self.ctrl.writer.set_blosc_compressor(params.value.as_i32());
2196 return ParamChangeResult::updates(vec![]);
2197 }
2198 if Some(reason) == self.hdf5_params.blosc_compress_level {
2199 self.ctrl
2200 .writer
2201 .set_blosc_compress_level(params.value.as_i32() as u32);
2202 return ParamChangeResult::updates(vec![]);
2203 }
2204 if Some(reason) == self.hdf5_params.nbit_precision {
2205 self.ctrl
2206 .writer
2207 .set_nbit_precision(params.value.as_i32() as u32);
2208 return ParamChangeResult::updates(vec![]);
2209 }
2210 if Some(reason) == self.hdf5_params.nbit_offset {
2211 self.ctrl
2212 .writer
2213 .set_nbit_offset(params.value.as_i32() as u32);
2214 return ParamChangeResult::updates(vec![]);
2215 }
2216 if Some(reason) == self.hdf5_params.jpeg_quality {
2217 self.ctrl
2218 .writer
2219 .set_jpeg_quality(params.value.as_i32() as u32);
2220 return ParamChangeResult::updates(vec![]);
2221 }
2222 if Some(reason) == self.hdf5_params.store_attributes {
2223 self.ctrl
2224 .writer
2225 .set_store_attributes(params.value.as_i32() != 0);
2226 return ParamChangeResult::updates(vec![]);
2227 }
2228 if Some(reason) == self.hdf5_params.store_performance {
2229 self.ctrl
2230 .writer
2231 .set_store_performance(params.value.as_i32() != 0);
2232 return ParamChangeResult::updates(vec![]);
2233 }
2234 if Some(reason) == self.hdf5_params.chunk_size_auto {
2236 self.ctrl
2237 .writer
2238 .set_chunk_size_auto(params.value.as_i32() != 0);
2239 return ParamChangeResult::updates(vec![]);
2240 }
2241 if Some(reason) == self.hdf5_params.n_row_chunks {
2242 self.ctrl
2243 .writer
2244 .set_n_row_chunks(params.value.as_i32().max(0) as usize);
2245 return ParamChangeResult::updates(vec![]);
2246 }
2247 if Some(reason) == self.hdf5_params.n_col_chunks {
2248 self.ctrl
2249 .writer
2250 .set_n_col_chunks(params.value.as_i32().max(0) as usize);
2251 return ParamChangeResult::updates(vec![]);
2252 }
2253 if Some(reason) == self.hdf5_params.n_frames_chunks {
2254 self.ctrl
2255 .writer
2256 .set_n_frames_chunks(params.value.as_i32().max(0) as usize);
2257 return ParamChangeResult::updates(vec![]);
2258 }
2259 if Some(reason) == self.hdf5_params.ndattr_chunk {
2260 self.ctrl
2261 .writer
2262 .set_ndattr_chunk(params.value.as_i32().max(1) as usize);
2263 return ParamChangeResult::updates(vec![]);
2264 }
2265 if Some(reason) == self.hdf5_params.n_extra_dims {
2267 self.ctrl
2268 .writer
2269 .set_n_extra_dims(params.value.as_i32().max(0) as usize);
2270 return ParamChangeResult::updates(vec![]);
2271 }
2272 for i in 0..MAX_EXTRA_DIMS {
2273 if Some(reason) == self.hdf5_params.extra_dim_size[i] {
2274 self.ctrl
2275 .writer
2276 .set_extra_dim_size(i, params.value.as_i32().max(1) as usize);
2277 return ParamChangeResult::updates(vec![]);
2278 }
2279 if Some(reason) == self.hdf5_params.extra_dim_name[i] {
2280 self.ctrl
2281 .writer
2282 .set_extra_dim_name(i, params.value.as_string().unwrap_or(""));
2283 return ParamChangeResult::updates(vec![]);
2284 }
2285 }
2286 if Some(reason) == self.hdf5_params.fill_value {
2287 self.ctrl.writer.set_fill_value(params.value.as_f64());
2288 return ParamChangeResult::updates(vec![]);
2289 }
2290 if Some(reason) == self.hdf5_params.dim_att_datasets {
2291 self.ctrl
2292 .writer
2293 .set_dim_att_datasets(params.value.as_i32() != 0);
2294 return ParamChangeResult::updates(vec![]);
2295 }
2296 if Some(reason) == self.hdf5_params.layout_filename {
2298 let path = params.value.as_string().unwrap_or("").to_string();
2299 self.ctrl.writer.set_layout_filename(&path);
2300 let mut updates = vec![];
2301 if let Some(idx) = self.hdf5_params.layout_valid {
2302 updates.push(ParamUpdate::int32(
2303 idx,
2304 if self.ctrl.writer.layout_valid { 1 } else { 0 },
2305 ));
2306 }
2307 if let Some(idx) = self.hdf5_params.layout_error_msg {
2308 updates.push(ParamUpdate::Octet {
2309 reason: idx,
2310 addr: 0,
2311 value: self.ctrl.writer.layout_error.clone(),
2312 });
2313 }
2314 return ParamChangeResult::updates(updates);
2315 }
2316 if Some(reason) == self.hdf5_params.swmr_mode {
2318 self.ctrl.writer.set_swmr_mode(params.value.as_i32() != 0);
2319 return ParamChangeResult::updates(vec![]);
2320 }
2321 if Some(reason) == self.hdf5_params.swmr_flush_now {
2322 if params.value.as_i32() != 0 {
2323 self.ctrl.writer.flush_swmr();
2324 let mut updates = vec![];
2325 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
2326 updates.push(ParamUpdate::int32(
2327 idx,
2328 self.ctrl.writer.swmr_cb_counter as i32,
2329 ));
2330 }
2331 return ParamChangeResult::updates(updates);
2332 }
2333 return ParamChangeResult::updates(vec![]);
2334 }
2335 if Some(reason) == self.hdf5_params.flush_nth_frame {
2336 self.ctrl
2337 .writer
2338 .set_flush_nth_frame(params.value.as_i32().max(0) as usize);
2339 return ParamChangeResult::updates(vec![]);
2340 }
2341 self.ctrl.on_param_change(reason, params)
2342 }
2343}
2344
2345#[cfg(test)]
2346mod tests {
2347 use super::*;
2348 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
2349 use std::sync::atomic::{AtomicU32, Ordering};
2350
2351 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
2352
2353 fn temp_path(prefix: &str) -> PathBuf {
2354 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
2355 std::env::temp_dir().join(format!("adcore_test_{}_{}.h5", prefix, n))
2356 }
2357
2358 #[test]
2359 fn test_write_single_frame() {
2360 let path = temp_path("hdf5_single");
2361 let mut writer = Hdf5Writer::new();
2362
2363 let mut arr = NDArray::new(
2364 vec![NDDimension::new(4), NDDimension::new(4)],
2365 NDDataType::UInt8,
2366 );
2367 if let NDDataBuffer::U8(ref mut v) = arr.data {
2368 for i in 0..16 {
2369 v[i] = i as u8;
2370 }
2371 }
2372
2373 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2374 writer.write_file(&arr).unwrap();
2375 writer.close_file().unwrap();
2376
2377 let h5 = H5File::open(&path).unwrap();
2379 let ds = h5.dataset("data").unwrap();
2380 assert_eq!(ds.shape(), vec![1, 4, 4]);
2381 let data: Vec<u8> = ds.read_raw().unwrap();
2382 assert_eq!(data[0], 0);
2383 assert_eq!(data[15], 15);
2384 drop(h5);
2385
2386 let mut reader = Hdf5Writer::new();
2387 reader.current_path = Some(path.clone());
2388 let read_arr = reader.read_file().unwrap();
2389 assert_eq!(read_arr.dims.len(), 3);
2390 assert_eq!(read_arr.dims[2].size, 1); std::fs::remove_file(&path).ok();
2393 }
2394
2395 #[test]
2396 fn test_write_multiple_frames() {
2397 let path = temp_path("hdf5_multi");
2398 let mut writer = Hdf5Writer::new();
2399
2400 let mut arr = NDArray::new(
2401 vec![NDDimension::new(4), NDDimension::new(4)],
2402 NDDataType::UInt8,
2403 );
2404 for f in 0..3u8 {
2406 if let NDDataBuffer::U8(ref mut v) = arr.data {
2407 for x in v.iter_mut() {
2408 *x = f;
2409 }
2410 }
2411 if f == 0 {
2412 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2413 }
2414 writer.write_file(&arr).unwrap();
2415 }
2416 writer.close_file().unwrap();
2417
2418 assert!(writer.supports_multiple_arrays());
2419 assert_eq!(writer.frame_count(), 3);
2420
2421 let data = std::fs::read(&path).unwrap();
2422 assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
2423
2424 let h5 = H5File::open(&path).unwrap();
2426 let names = h5.dataset_names();
2427 assert!(names.contains(&"data".to_string()));
2428 assert!(
2429 !names.contains(&"data_1".to_string()),
2430 "must not write per-frame datasets"
2431 );
2432 let ds = h5.dataset("data").unwrap();
2433 assert_eq!(
2434 ds.shape(),
2435 vec![3, 4, 4],
2436 "rank/shape must be [nframes,Y,X]"
2437 );
2438 let raw: Vec<u8> = ds.read_raw().unwrap();
2439 assert_eq!(raw.len(), 3 * 4 * 4);
2440 assert_eq!(raw[0], 0);
2442 assert_eq!(raw[16], 1);
2443 assert_eq!(raw[32], 2);
2444
2445 std::fs::remove_file(&path).ok();
2446 }
2447
2448 #[test]
2449 fn test_sub_frame_chunking() {
2450 let path = temp_path("hdf5_subchunk");
2454 let mut writer = Hdf5Writer::new();
2455 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
2460 vec![NDDimension::new(8), NDDimension::new(8)],
2461 NDDataType::UInt16,
2462 );
2463 for f in 0..3u16 {
2464 if let NDDataBuffer::U16(ref mut v) = arr.data {
2465 for (i, x) in v.iter_mut().enumerate() {
2466 *x = f * 1000 + i as u16;
2467 }
2468 }
2469 if f == 0 {
2470 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2471 }
2472 writer.write_file(&arr).unwrap();
2473 }
2474 writer.close_file().unwrap();
2475
2476 let h5 = H5File::open(&path).unwrap();
2477 let ds = h5.dataset("data").unwrap();
2478 assert_eq!(ds.shape(), vec![3, 8, 8], "shape must not be chunk-padded");
2479 assert_eq!(
2480 ds.chunk_dims(),
2481 Some(vec![1, 4, 4]),
2482 "chunk grid must be the sub-frame tile size"
2483 );
2484 let raw: Vec<u16> = ds.read_raw().unwrap();
2485 assert_eq!(raw.len(), 3 * 64);
2486 for f in 0..3u16 {
2487 for i in 0..64usize {
2488 assert_eq!(
2489 raw[f as usize * 64 + i],
2490 f * 1000 + i as u16,
2491 "frame {} elem {}",
2492 f,
2493 i
2494 );
2495 }
2496 }
2497
2498 std::fs::remove_file(&path).ok();
2499 }
2500
2501 #[test]
2502 fn test_sub_frame_chunking_with_compression() {
2503 let path = temp_path("hdf5_subchunk_zlib");
2506 let mut writer = Hdf5Writer::new();
2507 writer.set_chunk_size_auto(false);
2508 writer.set_n_row_chunks(4);
2509 writer.set_n_col_chunks(4);
2510 writer.set_compression_type(COMPRESS_ZLIB);
2511
2512 let mut arr = NDArray::new(
2513 vec![NDDimension::new(8), NDDimension::new(8)],
2514 NDDataType::UInt16,
2515 );
2516 for f in 0..2u16 {
2517 if let NDDataBuffer::U16(ref mut v) = arr.data {
2518 for (i, x) in v.iter_mut().enumerate() {
2519 *x = f * 100 + i as u16;
2520 }
2521 }
2522 if f == 0 {
2523 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2524 }
2525 writer.write_file(&arr).unwrap();
2526 }
2527 writer.close_file().unwrap();
2528
2529 let h5 = H5File::open(&path).unwrap();
2530 let ds = h5.dataset("data").unwrap();
2531 assert_eq!(ds.shape(), vec![2, 8, 8]);
2532 assert_eq!(ds.chunk_dims(), Some(vec![1, 4, 4]));
2533 let raw: Vec<u16> = ds.read_raw().unwrap();
2534 for f in 0..2u16 {
2535 for i in 0..64usize {
2536 assert_eq!(raw[f as usize * 64 + i], f * 100 + i as u16);
2537 }
2538 }
2539
2540 std::fs::remove_file(&path).ok();
2541 }
2542
2543 #[test]
2544 fn test_non_dividing_chunk_is_honored_and_extent_trimmed() {
2545 let path = temp_path("hdf5_subchunk_nd");
2549 let mut writer = Hdf5Writer::new();
2550 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(3); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
2555 vec![NDDimension::new(8), NDDimension::new(8)],
2556 NDDataType::UInt16,
2557 );
2558 if let NDDataBuffer::U16(ref mut v) = arr.data {
2559 for (i, x) in v.iter_mut().enumerate() {
2560 *x = i as u16;
2561 }
2562 }
2563 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2564 writer.write_file(&arr).unwrap();
2565 writer.write_file(&arr).unwrap();
2566 writer.close_file().unwrap();
2567
2568 let h5 = H5File::open(&path).unwrap();
2569 let ds = h5.dataset("data").unwrap();
2570 assert_eq!(ds.shape(), vec![2, 8, 8], "extent trimmed, not padded");
2571 assert_eq!(ds.chunk_dims(), Some(vec![1, 3, 4]));
2572 let raw: Vec<u16> = ds.read_raw().unwrap();
2573 assert_eq!(raw.len(), 2 * 64);
2574 for i in 0..64usize {
2575 assert_eq!(raw[i], i as u16);
2576 assert_eq!(raw[64 + i], i as u16);
2577 }
2578
2579 std::fs::remove_file(&path).ok();
2580 }
2581
2582 #[test]
2583 fn test_n_frames_chunks_band() {
2584 let path = temp_path("hdf5_framechunks");
2587 let mut writer = Hdf5Writer::new();
2588 writer.set_chunk_size_auto(false);
2589 writer.set_n_frames_chunks(2); let mut arr = NDArray::new(
2592 vec![NDDimension::new(4), NDDimension::new(4)],
2593 NDDataType::UInt16,
2594 );
2595 for f in 0..5u16 {
2597 if let NDDataBuffer::U16(ref mut v) = arr.data {
2598 for (i, x) in v.iter_mut().enumerate() {
2599 *x = f * 1000 + i as u16;
2600 }
2601 }
2602 if f == 0 {
2603 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2604 }
2605 writer.write_file(&arr).unwrap();
2606 }
2607 writer.close_file().unwrap();
2608
2609 let h5 = H5File::open(&path).unwrap();
2610 let ds = h5.dataset("data").unwrap();
2611 assert_eq!(ds.shape(), vec![5, 4, 4], "exact frame count, no padding");
2612 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
2613 let raw: Vec<u16> = ds.read_raw().unwrap();
2614 for f in 0..5u16 {
2615 for i in 0..16usize {
2616 assert_eq!(raw[f as usize * 16 + i], f * 1000 + i as u16);
2617 }
2618 }
2619
2620 std::fs::remove_file(&path).ok();
2621 }
2622
2623 #[test]
2624 fn test_frames_chunks_with_sub_frame_tiles() {
2625 let path = temp_path("hdf5_full_chunk");
2629 let mut writer = Hdf5Writer::new();
2630 writer.set_chunk_size_auto(false);
2631 writer.set_n_frames_chunks(2); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
2636 vec![NDDimension::new(8), NDDimension::new(8)],
2637 NDDataType::UInt16,
2638 );
2639 for f in 0..3u16 {
2641 if let NDDataBuffer::U16(ref mut v) = arr.data {
2642 for (i, x) in v.iter_mut().enumerate() {
2643 *x = f * 1000 + i as u16;
2644 }
2645 }
2646 if f == 0 {
2647 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2648 }
2649 writer.write_file(&arr).unwrap();
2650 }
2651 writer.close_file().unwrap();
2652
2653 let h5 = H5File::open(&path).unwrap();
2654 let ds = h5.dataset("data").unwrap();
2655 assert_eq!(ds.shape(), vec![3, 8, 8], "exact frame count");
2656 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
2657 let raw: Vec<u16> = ds.read_raw().unwrap();
2658 assert_eq!(raw.len(), 3 * 64);
2659 for f in 0..3u16 {
2660 for i in 0..64usize {
2661 assert_eq!(
2662 raw[f as usize * 64 + i],
2663 f * 1000 + i as u16,
2664 "frame {} elem {}",
2665 f,
2666 i
2667 );
2668 }
2669 }
2670
2671 std::fs::remove_file(&path).ok();
2672 }
2673
2674 #[test]
2675 fn test_attribute_datasets() {
2676 let path = temp_path("hdf5_attr_ds");
2677 let mut writer = Hdf5Writer::new();
2678
2679 let mk = |exposure: f64, count: i32| {
2680 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2681 arr.attributes.add(NDAttribute::new_static(
2682 "exposure",
2683 "",
2684 NDAttrSource::Driver,
2685 NDAttrValue::Float64(exposure),
2686 ));
2687 arr.attributes.add(NDAttribute::new_static(
2688 "count",
2689 "",
2690 NDAttrSource::Driver,
2691 NDAttrValue::Int32(count),
2692 ));
2693 arr
2694 };
2695
2696 let a0 = mk(0.5, 10);
2697 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
2698 writer.write_file(&a0).unwrap();
2699 writer.write_file(&mk(0.75, 20)).unwrap();
2700 writer.write_file(&mk(1.25, 30)).unwrap();
2701 writer.close_file().unwrap();
2702
2703 let h5 = H5File::open(&path).unwrap();
2704 let exp = h5.dataset("NDAttributes/exposure").unwrap();
2706 assert_eq!(exp.shape(), vec![3]);
2707 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
2708 assert_eq!(exp_vals, vec![0.5, 0.75, 1.25]);
2709
2710 let cnt = h5.dataset("NDAttributes/count").unwrap();
2711 assert_eq!(cnt.shape(), vec![3]);
2712 let cnt_vals: Vec<i32> = cnt.read_raw().unwrap();
2714 assert_eq!(cnt_vals, vec![10, 20, 30]);
2715
2716 std::fs::remove_file(&path).ok();
2717 }
2718
2719 #[test]
2720 fn test_fill_value_recorded_on_dataset() {
2721 let path = temp_path("hdf5_fill");
2726 let mut writer = Hdf5Writer::new();
2727 writer.set_fill_value(7.5);
2728
2729 let arr = NDArray::new(
2730 vec![NDDimension::new(4), NDDimension::new(4)],
2731 NDDataType::UInt16,
2732 );
2733 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2734 writer.write_file(&arr).unwrap();
2735 writer.close_file().unwrap();
2736
2737 let h5 = H5File::open(&path).unwrap();
2738 let ds = h5.dataset("data").unwrap();
2739 let fv: f64 = ds.attr("HDF5_fillValue").unwrap().read_numeric().unwrap();
2740 assert_eq!(fv, 7.5);
2741 std::fs::remove_file(&path).ok();
2742
2743 let path2 = temp_path("hdf5_fill_dcpl");
2746 {
2747 let f = H5File::create(&path2).unwrap();
2748 let _ = f
2749 .new_dataset::<i32>()
2750 .shape(&[8][..])
2751 .fill_value(42i32)
2752 .create("unwritten")
2753 .unwrap();
2754 }
2755 let h5b = H5File::open(&path2).unwrap();
2756 let vals: Vec<i32> = h5b.dataset("unwritten").unwrap().read_raw().unwrap();
2757 assert_eq!(vals, vec![42i32; 8]);
2758 std::fs::remove_file(&path2).ok();
2759 }
2760
2761 #[test]
2762 fn test_performance_dataset() {
2763 let path = temp_path("hdf5_perf");
2764 let mut writer = Hdf5Writer::new();
2765 writer.set_store_performance(true);
2766
2767 let arr = NDArray::new(
2768 vec![NDDimension::new(8), NDDimension::new(8)],
2769 NDDataType::UInt16,
2770 );
2771 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2772 writer.write_file(&arr).unwrap();
2773 writer.write_file(&arr).unwrap();
2774 writer.close_file().unwrap();
2775
2776 let h5 = H5File::open(&path).unwrap();
2777 let ts = h5.dataset("performance/timestamp").unwrap();
2778 assert_eq!(ts.shape(), vec![2, 5]);
2779 let vals: Vec<f64> = ts.read_raw().unwrap();
2780 assert_eq!(vals.len(), 10);
2781
2782 std::fs::remove_file(&path).ok();
2783 }
2784
2785 #[test]
2786 fn test_roundtrip_all_types() {
2787 macro_rules! roundtrip {
2788 ($name:expr, $dt:expr, $variant:ident, $ty:ty, $vals:expr) => {{
2789 let path = temp_path($name);
2790 let mut writer = Hdf5Writer::new();
2791 let mut arr = NDArray::new(vec![NDDimension::new(4)], $dt);
2792 if let NDDataBuffer::$variant(ref mut v) = arr.data {
2793 let src: Vec<$ty> = $vals;
2794 v.copy_from_slice(&src);
2795 }
2796 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2797 writer.write_file(&arr).unwrap();
2798 writer.close_file().unwrap();
2799
2800 let mut reader = Hdf5Writer::new();
2801 reader.current_path = Some(path.clone());
2802 let r = reader.read_file().unwrap();
2803 assert_eq!(r.data.data_type(), $dt, "type for {}", $name);
2804 if let NDDataBuffer::$variant(ref v) = r.data {
2805 let src: Vec<$ty> = $vals;
2806 assert_eq!(v, &src, "values for {}", $name);
2807 } else {
2808 panic!("wrong buffer variant for {}", $name);
2809 }
2810 std::fs::remove_file(&path).ok();
2811 }};
2812 }
2813
2814 roundtrip!("rt_i8", NDDataType::Int8, I8, i8, vec![-1, 0, 1, 127]);
2815 roundtrip!("rt_u8", NDDataType::UInt8, U8, u8, vec![0, 1, 200, 255]);
2816 roundtrip!(
2817 "rt_i16",
2818 NDDataType::Int16,
2819 I16,
2820 i16,
2821 vec![-32768, -1, 1, 32767]
2822 );
2823 roundtrip!(
2824 "rt_u16",
2825 NDDataType::UInt16,
2826 U16,
2827 u16,
2828 vec![0, 1, 40000, 65535]
2829 );
2830 roundtrip!(
2831 "rt_i32",
2832 NDDataType::Int32,
2833 I32,
2834 i32,
2835 vec![i32::MIN, -1, 1, i32::MAX]
2836 );
2837 roundtrip!(
2838 "rt_u32",
2839 NDDataType::UInt32,
2840 U32,
2841 u32,
2842 vec![0, 1, 3_000_000_000, u32::MAX]
2843 );
2844 roundtrip!(
2845 "rt_i64",
2846 NDDataType::Int64,
2847 I64,
2848 i64,
2849 vec![i64::MIN, -1, 1, i64::MAX]
2850 );
2851 roundtrip!(
2852 "rt_u64",
2853 NDDataType::UInt64,
2854 U64,
2855 u64,
2856 vec![0, 1, 9_000_000_000, u64::MAX]
2857 );
2858 roundtrip!(
2859 "rt_f32",
2860 NDDataType::Float32,
2861 F32,
2862 f32,
2863 vec![-1.5, 0.0, 2.25, 3.75]
2864 );
2865 roundtrip!(
2866 "rt_f64",
2867 NDDataType::Float64,
2868 F64,
2869 f64,
2870 vec![-1.5, 0.0, 2.25, 3.75]
2871 );
2872 }
2873
2874 #[test]
2875 fn test_deflate_compressed_write() {
2876 let path = temp_path("hdf5_deflate");
2877 let mut writer = Hdf5Writer::new();
2878 writer.set_compression_type(COMPRESS_ZLIB);
2879 writer.set_z_compress_level(6);
2880
2881 let mut arr = NDArray::new(
2882 vec![NDDimension::new(64), NDDimension::new(64)],
2883 NDDataType::UInt16,
2884 );
2885 if let NDDataBuffer::U16(ref mut v) = arr.data {
2886 for i in 0..v.len() {
2887 v[i] = (i % 256) as u16;
2888 }
2889 }
2890
2891 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2892 writer.write_file(&arr).unwrap();
2893 writer.close_file().unwrap();
2894
2895 let file_size = std::fs::metadata(&path).unwrap().len();
2896 assert!(
2897 file_size < 8192,
2898 "compressed file should be smaller than raw data"
2899 );
2900
2901 let h5file = H5File::open(&path).unwrap();
2902 let ds = h5file.dataset("data").unwrap();
2903 let data: Vec<u16> = ds.read_raw().unwrap();
2904 assert_eq!(data.len(), 64 * 64);
2905 assert_eq!(data[0], 0);
2906 assert_eq!(data[255], 255);
2907 assert_eq!(data[256], 0);
2908
2909 std::fs::remove_file(&path).ok();
2910 }
2911
2912 #[test]
2913 fn test_lz4_compressed_write() {
2914 let path = temp_path("hdf5_lz4");
2915 let mut writer = Hdf5Writer::new();
2916 writer.set_compression_type(COMPRESS_LZ4);
2917
2918 let mut arr = NDArray::new(
2919 vec![NDDimension::new(32), NDDimension::new(32)],
2920 NDDataType::UInt8,
2921 );
2922 if let NDDataBuffer::U8(ref mut v) = arr.data {
2923 for i in 0..v.len() {
2924 v[i] = (i % 4) as u8;
2925 }
2926 }
2927
2928 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2929 writer.write_file(&arr).unwrap();
2930 writer.close_file().unwrap();
2931
2932 let h5file = H5File::open(&path).unwrap();
2933 let ds = h5file.dataset("data").unwrap();
2934 let data: Vec<u8> = ds.read_raw().unwrap();
2935 assert_eq!(data.len(), 32 * 32);
2936 assert_eq!(data[0], 0);
2937 assert_eq!(data[3], 3);
2938
2939 std::fs::remove_file(&path).ok();
2940 }
2941
2942 #[test]
2943 fn test_bitshuffle_compressed_write() {
2944 let path = temp_path("hdf5_bshuf");
2945 let mut writer = Hdf5Writer::new();
2946 writer.set_compression_type(COMPRESS_BSHUF);
2947
2948 let mut arr = NDArray::new(
2949 vec![NDDimension::new(64), NDDimension::new(64)],
2950 NDDataType::UInt16,
2951 );
2952 if let NDDataBuffer::U16(ref mut v) = arr.data {
2953 for i in 0..v.len() {
2954 v[i] = (i % 8) as u16;
2955 }
2956 }
2957
2958 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
2959 writer.write_file(&arr).unwrap();
2960 writer.close_file().unwrap();
2961
2962 let h5file = H5File::open(&path).unwrap();
2963 let ds = h5file.dataset("data").unwrap();
2964 let data: Vec<u16> = ds.read_raw().unwrap();
2965 assert_eq!(data.len(), 64 * 64);
2966 assert_eq!(data[0], 0);
2967 assert_eq!(data[9], 1);
2968
2969 std::fs::remove_file(&path).ok();
2970 }
2971
2972 #[test]
2973 fn test_chunk_geometry_recorded() {
2974 let path = temp_path("hdf5_chunkgeom");
2977 let mut writer = Hdf5Writer::new();
2978 writer.set_chunk_size_auto(false);
2979 writer.set_n_row_chunks(4);
2980 writer.set_n_col_chunks(2);
2981 writer.set_n_frames_chunks(3);
2982
2983 let mut arr = NDArray::new(
2984 vec![NDDimension::new(8), NDDimension::new(8)],
2985 NDDataType::UInt16,
2986 );
2987 if let NDDataBuffer::U16(ref mut v) = arr.data {
2988 for i in 0..v.len() {
2989 v[i] = i as u16;
2990 }
2991 }
2992
2993 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
2994 writer.write_file(&arr).unwrap();
2995 writer.write_file(&arr).unwrap();
2996 writer.close_file().unwrap();
2997
2998 let h5 = H5File::open(&path).unwrap();
2999 let ds = h5.dataset("data").unwrap();
3000 assert_eq!(ds.shape(), vec![2, 8, 8]);
3001 let data: Vec<u16> = ds.read_raw().unwrap();
3003 assert_eq!(data.len(), 2 * 64);
3004 for i in 0..64usize {
3005 assert_eq!(data[i], i as u16, "frame0 element {}", i);
3006 assert_eq!(data[64 + i], i as u16, "frame1 element {}", i);
3007 }
3008 assert_eq!(
3010 ds.attr("HDF5_nRowChunks")
3011 .unwrap()
3012 .read_numeric::<i32>()
3013 .unwrap(),
3014 4
3015 );
3016 assert_eq!(
3017 ds.attr("HDF5_nColChunks")
3018 .unwrap()
3019 .read_numeric::<i32>()
3020 .unwrap(),
3021 2
3022 );
3023 assert_eq!(
3024 ds.attr("HDF5_nFramesChunks")
3025 .unwrap()
3026 .read_numeric::<i32>()
3027 .unwrap(),
3028 3
3029 );
3030
3031 std::fs::remove_file(&path).ok();
3032 }
3033
3034 #[test]
3035 fn test_extra_dimensions_layout() {
3036 let path = temp_path("hdf5_extradims");
3040 let mut writer = Hdf5Writer::new();
3041 writer.set_n_extra_dims(2);
3042 writer.set_extra_dim_size(0, 2);
3043 writer.set_extra_dim_size(1, 3);
3044 writer.set_extra_dim_name(0, "scanY");
3045 writer.set_extra_dim_name(1, "scanX");
3046
3047 let mut arr = NDArray::new(
3048 vec![NDDimension::new(4), NDDimension::new(4)],
3049 NDDataType::UInt16,
3050 );
3051 for f in 0..6u16 {
3052 if let NDDataBuffer::U16(ref mut v) = arr.data {
3053 for x in v.iter_mut() {
3054 *x = f;
3055 }
3056 }
3057 if f == 0 {
3058 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3059 }
3060 writer.write_file(&arr).unwrap();
3061 }
3062 writer.close_file().unwrap();
3063
3064 let h5 = H5File::open(&path).unwrap();
3065 let ds = h5.dataset("data").unwrap();
3066 assert_eq!(ds.shape(), vec![6, 4, 4]);
3068 let data: Vec<u16> = ds.read_raw().unwrap();
3069 assert_eq!(data.len(), 6 * 16);
3070 for f in 0..6usize {
3071 for i in 0..16usize {
3072 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
3073 }
3074 }
3075 assert_eq!(
3077 ds.attr("HDF5_nExtraDims")
3078 .unwrap()
3079 .read_numeric::<i32>()
3080 .unwrap(),
3081 2
3082 );
3083 assert_eq!(
3084 ds.attr("HDF5_extraDimSize0")
3085 .unwrap()
3086 .read_numeric::<i32>()
3087 .unwrap(),
3088 2
3089 );
3090 assert_eq!(
3091 ds.attr("HDF5_extraDimSize1")
3092 .unwrap()
3093 .read_numeric::<i32>()
3094 .unwrap(),
3095 3
3096 );
3097 assert_eq!(
3098 ds.attr("HDF5_extraDimName0")
3099 .unwrap()
3100 .read_string()
3101 .unwrap(),
3102 "scanY"
3103 );
3104
3105 std::fs::remove_file(&path).ok();
3106 }
3107
3108 #[test]
3109 fn test_swmr_streaming() {
3110 let path = temp_path("hdf5_swmr");
3111 let mut writer = Hdf5Writer::new();
3112 writer.set_swmr_mode(true);
3113 writer.set_flush_nth_frame(2);
3114
3115 let arr = NDArray::new(
3116 vec![NDDimension::new(8), NDDimension::new(8)],
3117 NDDataType::Float32,
3118 );
3119
3120 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3121 writer.write_file(&arr).unwrap();
3122 writer.write_file(&arr).unwrap(); writer.write_file(&arr).unwrap();
3124 writer.close_file().unwrap();
3125
3126 assert_eq!(writer.frame_count(), 3);
3127
3128 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
3130 let shape = reader.dataset_shape("data").unwrap();
3131 assert_eq!(shape[0], 3); assert_eq!(shape[1], 8);
3133 assert_eq!(shape[2], 8);
3134
3135 let data: Vec<f32> = reader.read_dataset("data").unwrap();
3136 assert_eq!(data.len(), 3 * 8 * 8);
3137
3138 std::fs::remove_file(&path).ok();
3139 }
3140
3141 #[test]
3142 fn test_swmr_compression_is_applied() {
3143 let path = temp_path("hdf5_swmr_comp");
3147 let mut writer = Hdf5Writer::new();
3148 writer.set_swmr_mode(true);
3149 writer.set_compression_type(COMPRESS_ZLIB);
3150
3151 let arr = NDArray::new(
3152 vec![NDDimension::new(8), NDDimension::new(8)],
3153 NDDataType::UInt16,
3154 );
3155 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3156 assert!(
3157 !writer.swmr_compression_dropped(),
3158 "SWMR+ZLIB must apply compression, not drop it"
3159 );
3160 writer.write_file(&arr).unwrap();
3161 writer.write_file(&arr).unwrap();
3162 writer.close_file().unwrap();
3163
3164 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
3166 let shape = reader.dataset_shape("data").unwrap();
3167 assert_eq!(shape, vec![2, 8, 8]);
3168 let data: Vec<u16> = reader.read_dataset("data").unwrap();
3169 assert_eq!(data.len(), 2 * 8 * 8);
3170
3171 std::fs::remove_file(&path).ok();
3172 }
3173
3174 #[test]
3175 fn test_layout_xml_param() {
3176 let mut writer = Hdf5Writer::new();
3178 let dir = std::env::temp_dir();
3179 let good = dir.join("adcore_layout_good.xml");
3180 std::fs::write(
3181 &good,
3182 r#"<hdf5_layout><group name="entry"><dataset name="data" source="detector" det_default="true"/></group></hdf5_layout>"#,
3183 )
3184 .unwrap();
3185 assert!(writer.set_layout_filename(good.to_str().unwrap()));
3186 assert!(writer.layout_valid);
3187 assert!(writer.layout_error.is_empty());
3188
3189 let bad = dir.join("adcore_layout_bad.xml");
3190 std::fs::write(&bad, r#"<not_a_layout/>"#).unwrap();
3191 assert!(!writer.set_layout_filename(bad.to_str().unwrap()));
3192 assert!(!writer.layout_valid);
3193 assert!(!writer.layout_error.is_empty());
3194
3195 std::fs::remove_file(&good).ok();
3196 std::fs::remove_file(&bad).ok();
3197 }
3198
3199 #[test]
3200 fn test_layout_xml_places_dataset_in_nested_tree() {
3201 let dir = std::env::temp_dir();
3207 let layout = dir.join("adcore_layout_nested.xml");
3208 std::fs::write(
3209 &layout,
3210 r#"<hdf5_layout>
3211 <group name="entry">
3212 <group name="instrument">
3213 <group name="detector">
3214 <dataset name="data" source="detector" det_default="true">
3215 <attribute name="signal" source="constant" value="1" type="int"/>
3216 </dataset>
3217 </group>
3218 <group name="NDAttributes" ndattr_default="true"/>
3219 <group name="performance">
3220 <dataset name="timestamp"/>
3221 </group>
3222 </group>
3223 </group>
3224 </hdf5_layout>"#,
3225 )
3226 .unwrap();
3227
3228 let path = temp_path("hdf5_layout_nested");
3229 let mut writer = Hdf5Writer::new();
3230 writer.set_store_performance(true);
3231 assert!(
3232 writer.set_layout_filename(layout.to_str().unwrap()),
3233 "layout XML must parse: {}",
3234 writer.layout_error
3235 );
3236
3237 let mk = |fill: f64| {
3238 let mut arr = NDArray::new(
3239 vec![NDDimension::new(4), NDDimension::new(4)],
3240 NDDataType::UInt16,
3241 );
3242 arr.attributes.add(NDAttribute::new_static(
3243 "exposure",
3244 "",
3245 NDAttrSource::Driver,
3246 NDAttrValue::Float64(fill),
3247 ));
3248 arr
3249 };
3250
3251 let a0 = mk(0.5);
3252 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
3253 writer.write_file(&a0).unwrap();
3254 writer.write_file(&mk(0.75)).unwrap();
3255 writer.close_file().unwrap();
3256
3257 let h5 = H5File::open(&path).unwrap();
3258 let names = h5.dataset_names();
3259 assert!(
3261 names.contains(&"entry/instrument/detector/data".to_string()),
3262 "image dataset must be at the nested layout path; got {:?}",
3263 names
3264 );
3265 assert!(
3266 !names.contains(&"data".to_string()),
3267 "must not also write a flat-root `data` dataset"
3268 );
3269 let img = h5.dataset("entry/instrument/detector/data").unwrap();
3270 assert_eq!(img.shape(), vec![2, 4, 4]);
3271 assert_eq!(
3273 img.attr("signal").unwrap().read_numeric::<i64>().unwrap(),
3274 1
3275 );
3276 assert!(
3278 names.contains(&"entry/instrument/NDAttributes/exposure".to_string()),
3279 "NDAttribute dataset must be under the layout ndattr group; got {:?}",
3280 names
3281 );
3282 assert!(
3284 names.contains(&"entry/instrument/performance/timestamp".to_string()),
3285 "performance dataset must be under the layout group; got {:?}",
3286 names
3287 );
3288
3289 drop(h5);
3291 let mut reader = Hdf5Writer::new();
3292 assert!(reader.set_layout_filename(layout.to_str().unwrap()));
3293 reader.current_path = Some(path.clone());
3294 let read_arr = reader.read_file().unwrap();
3295 assert_eq!(read_arr.dims.len(), 3);
3296
3297 std::fs::remove_file(&path).ok();
3298 std::fs::remove_file(&layout).ok();
3299 }
3300
3301 #[test]
3302 fn test_layout_hardlink_is_materialised() {
3303 let dir = std::env::temp_dir();
3309 let layout = dir.join("adcore_layout_hardlink.xml");
3310 std::fs::write(
3311 &layout,
3312 r#"<hdf5_layout>
3313 <group name="entry">
3314 <group name="data">
3315 <dataset name="data" source="detector" det_default="true"/>
3316 <hardlink name="data_alias" target="/entry/data/data"/>
3317 </group>
3318 </group>
3319 </hdf5_layout>"#,
3320 )
3321 .unwrap();
3322
3323 let path = temp_path("hdf5_layout_hardlink");
3324 let mut writer = Hdf5Writer::new();
3325 assert!(
3326 writer.set_layout_filename(layout.to_str().unwrap()),
3327 "layout XML must parse: {}",
3328 writer.layout_error
3329 );
3330
3331 let arr = NDArray::new(
3332 vec![NDDimension::new(4), NDDimension::new(4)],
3333 NDDataType::UInt16,
3334 );
3335 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3336 writer.write_file(&arr).unwrap();
3337 writer.close_file().unwrap();
3338
3339 let h5 = H5File::open(&path).unwrap();
3340 let names = h5.dataset_names();
3341 assert!(
3343 names.contains(&"entry/data/data".to_string()),
3344 "image dataset must exist at the layout path; got {:?}",
3345 names
3346 );
3347 assert!(
3349 names.contains(&"entry/data/data_alias".to_string()),
3350 "layout <hardlink> must be materialised as a hard link; got {:?}",
3351 names
3352 );
3353 let alias = h5.dataset("entry/data/data_alias").unwrap();
3355 let orig = h5.dataset("entry/data/data").unwrap();
3356 assert_eq!(alias.shape(), orig.shape());
3357
3358 drop(h5);
3359 std::fs::remove_file(&path).ok();
3360 std::fs::remove_file(&layout).ok();
3361 }
3362
3363 #[test]
3364 fn test_swmr_layout_hardlink_is_materialised() {
3365 let dir = std::env::temp_dir();
3377 let layout = dir.join("adcore_swmr_layout_hardlink.xml");
3378 std::fs::write(
3379 &layout,
3380 r#"<hdf5_layout>
3381 <group name="entry">
3382 <group name="data">
3383 <dataset name="data" source="detector" det_default="true"/>
3384 <hardlink name="data_alias" target="/entry/data/data"/>
3385 </group>
3386 </group>
3387 </hdf5_layout>"#,
3388 )
3389 .unwrap();
3390
3391 let path = temp_path("hdf5_swmr_layout_hardlink");
3392 let mut writer = Hdf5Writer::new();
3393 writer.set_swmr_mode(true);
3394 assert!(
3395 writer.set_layout_filename(layout.to_str().unwrap()),
3396 "layout XML must parse: {}",
3397 writer.layout_error
3398 );
3399
3400 let mut arr = NDArray::new(
3401 vec![NDDimension::new(4), NDDimension::new(4)],
3402 NDDataType::UInt16,
3403 );
3404 if let NDDataBuffer::U16(ref mut v) = arr.data {
3405 for (i, x) in v.iter_mut().enumerate() {
3406 *x = i as u16;
3407 }
3408 }
3409 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3410 assert!(
3411 writer.is_swmr_active(),
3412 "writer must be in SWMR mode for this test"
3413 );
3414 writer.write_file(&arr).unwrap();
3415 writer.write_file(&arr).unwrap();
3416 writer.close_file().unwrap();
3417
3418 let h5 = H5File::open(&path).unwrap();
3419 let names = h5.dataset_names();
3420 assert!(
3422 names.contains(&"entry/data/data".to_string()),
3423 "SWMR image dataset must exist at the nested layout path; got {:?}",
3424 names
3425 );
3426 assert!(
3428 names.contains(&"entry/data/data_alias".to_string()),
3429 "SWMR layout <hardlink> must be materialised as a hard link; got {:?}",
3430 names
3431 );
3432 let alias = h5.dataset("entry/data/data_alias").unwrap();
3434 let orig = h5.dataset("entry/data/data").unwrap();
3435 assert_eq!(alias.shape(), orig.shape());
3436 assert_eq!(orig.shape(), vec![2, 4, 4]);
3437
3438 drop(h5);
3439 std::fs::remove_file(&path).ok();
3440 std::fs::remove_file(&layout).ok();
3441 }
3442
3443 #[test]
3444 fn test_swmr_layout_nested_dataset_placement() {
3445 let dir = std::env::temp_dir();
3452 let layout = dir.join("adcore_swmr_layout_nested.xml");
3453 std::fs::write(
3454 &layout,
3455 r#"<hdf5_layout>
3456 <group name="entry">
3457 <group name="instrument">
3458 <group name="detector">
3459 <dataset name="data" source="detector" det_default="true">
3460 <attribute name="signal" source="constant" value="1" type="int"/>
3461 </dataset>
3462 <hardlink name="data_alias" target="/entry/instrument/detector/data"/>
3463 </group>
3464 </group>
3465 <group name="empty_placeholder"/>
3466 </group>
3467 </hdf5_layout>"#,
3468 )
3469 .unwrap();
3470
3471 let path = temp_path("hdf5_swmr_layout_nested");
3472 let mut writer = Hdf5Writer::new();
3473 writer.set_swmr_mode(true);
3474 assert!(
3475 writer.set_layout_filename(layout.to_str().unwrap()),
3476 "layout XML must parse: {}",
3477 writer.layout_error
3478 );
3479
3480 let mut arr = NDArray::new(
3481 vec![NDDimension::new(4), NDDimension::new(4)],
3482 NDDataType::UInt16,
3483 );
3484 if let NDDataBuffer::U16(ref mut v) = arr.data {
3485 for (i, x) in v.iter_mut().enumerate() {
3486 *x = (i * 3) as u16;
3487 }
3488 }
3489 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3490 assert!(
3491 writer.is_swmr_active(),
3492 "writer must be in SWMR mode for this test"
3493 );
3494 writer.write_file(&arr).unwrap();
3495 writer.write_file(&arr).unwrap();
3496 writer.close_file().unwrap();
3497
3498 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
3501 let names = reader.dataset_names();
3502 assert!(
3504 names.contains(&"entry/instrument/detector/data".to_string()),
3505 "SWMR image dataset must live at the nested layout path; got {:?}",
3506 names
3507 );
3508 assert!(
3509 !names.contains(&"data".to_string()),
3510 "SWMR image dataset must NOT remain at the flat root; got {:?}",
3511 names
3512 );
3513 assert!(
3515 reader.has_group("entry/empty_placeholder"),
3516 "empty layout group must be materialised; groups {:?}",
3517 reader.group_paths()
3518 );
3519 assert!(
3521 names.contains(&"entry/instrument/detector/data_alias".to_string()),
3522 "SWMR layout <hardlink> must resolve to the nested dataset; got {:?}",
3523 names
3524 );
3525 let nested = reader
3526 .dataset_shape("entry/instrument/detector/data")
3527 .unwrap();
3528 let alias = reader
3529 .dataset_shape("entry/instrument/detector/data_alias")
3530 .unwrap();
3531 assert_eq!(nested, vec![2, 4, 4]);
3532 assert_eq!(alias, nested, "hardlink alias must share the target shape");
3533 let via_nested: Vec<u16> = reader
3535 .read_dataset("entry/instrument/detector/data")
3536 .unwrap();
3537 let via_alias: Vec<u16> = reader
3538 .read_dataset("entry/instrument/detector/data_alias")
3539 .unwrap();
3540 assert_eq!(via_nested, via_alias);
3541 assert_eq!(via_nested.len(), 2 * 4 * 4);
3542 assert_eq!(
3544 reader
3545 .dataset_attr_names("entry/instrument/detector/data")
3546 .unwrap(),
3547 vec!["signal".to_string()],
3548 );
3549
3550 drop(reader);
3551 std::fs::remove_file(&path).ok();
3552 std::fs::remove_file(&layout).ok();
3553 }
3554
3555 #[test]
3556 fn test_no_layout_keeps_flat_root_default() {
3557 let path = temp_path("hdf5_flat_default");
3559 let mut writer = Hdf5Writer::new();
3560 let arr = NDArray::new(
3561 vec![NDDimension::new(4), NDDimension::new(4)],
3562 NDDataType::UInt8,
3563 );
3564 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3565 writer.write_file(&arr).unwrap();
3566 writer.close_file().unwrap();
3567
3568 let h5 = H5File::open(&path).unwrap();
3569 assert!(h5.dataset_names().contains(&"data".to_string()));
3570 std::fs::remove_file(&path).ok();
3571 }
3572}