1use std::path::Path;
25
26use ndarray::Array3;
27
28use crate::error::IoError;
29
30#[derive(Debug, Clone)]
32pub struct NexusMetadata {
33 pub has_histogram: bool,
35 pub has_events: bool,
37 pub histogram_shape: Option<[usize; 4]>,
39 pub n_events: Option<usize>,
41 pub flight_path_m: Option<f64>,
43 pub tof_offset_ns: Option<f64>,
45 pub tof_edges_ns: Option<Vec<f64>>,
47}
48
49#[derive(Debug, Clone)]
51pub struct Hdf5TreeEntry {
52 pub path: String,
54 pub kind: Hdf5EntryKind,
56 pub shape: Option<Vec<usize>>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Hdf5EntryKind {
63 Group,
64 Dataset,
65}
66
67#[derive(Debug, Clone)]
69pub struct NexusHistogramData {
70 pub counts: Array3<f64>,
72 pub tof_edges_us: Vec<f64>,
74 pub flight_path_m: Option<f64>,
76 pub dead_pixels: Option<ndarray::Array2<bool>>,
78 pub n_rotation_angles: usize,
80 pub event_stats: Option<EventRetentionStats>,
82}
83
84#[derive(Debug, Clone)]
86pub struct EventRetentionStats {
87 pub total: usize,
89 pub kept: usize,
91 pub dropped_non_finite: usize,
98 pub dropped_tof_range: usize,
100 pub dropped_spatial: usize,
102}
103
104pub fn probe_nexus(path: &Path) -> Result<NexusMetadata, IoError> {
109 let file = hdf5::File::open(path).map_err(|e| {
110 IoError::FileNotFound(
111 path.display().to_string(),
112 std::io::Error::other(e.to_string()),
113 )
114 })?;
115
116 let entry = file
117 .group("entry")
118 .map_err(|e| IoError::InvalidParameter(format!("Missing /entry group: {e}")))?;
119
120 let (has_histogram, histogram_shape, tof_edges_ns) = probe_histogram_group(&entry);
122
123 let (has_events, n_events) = probe_event_group(&entry);
125
126 let flight_path_m = read_f64_attr(&entry, "flight_path_m");
128 let tof_offset_ns = read_f64_attr(&entry, "tof_offset_ns");
129
130 Ok(NexusMetadata {
131 has_histogram,
132 has_events,
133 histogram_shape,
134 n_events,
135 flight_path_m,
136 tof_offset_ns,
137 tof_edges_ns,
138 })
139}
140
141pub fn load_nexus_histogram(path: &Path) -> Result<NexusHistogramData, IoError> {
147 let file = hdf5::File::open(path).map_err(|e| {
148 IoError::FileNotFound(
149 path.display().to_string(),
150 std::io::Error::other(e.to_string()),
151 )
152 })?;
153
154 let entry = file
155 .group("entry")
156 .map_err(|e| IoError::InvalidParameter(format!("Missing /entry group: {e}")))?;
157
158 let hist_group = entry
159 .group("histogram")
160 .map_err(|e| IoError::InvalidParameter(format!("Missing /entry/histogram group: {e}")))?;
161
162 let counts_ds = hist_group.dataset("counts").map_err(|e| {
164 IoError::InvalidParameter(format!("Missing /entry/histogram/counts dataset: {e}"))
165 })?;
166
167 let shape = counts_ds.shape();
168 if shape.len() != 4 {
169 return Err(IoError::ShapeMismatch(format!(
170 "Expected 4D histogram counts, got {}D",
171 shape.len()
172 )));
173 }
174
175 let counts_u64: ndarray::Array4<u64> = counts_ds
176 .read()
177 .map_err(|e| IoError::InvalidParameter(format!("Failed to read histogram counts: {e}")))?;
178
179 let n_rot = shape[0];
183 if n_rot > 1 {
184 eprintln!(
185 "Warning: NeXus histogram has {n_rot} rotation angles — summing into a single \
186 volume. Multi-angle analysis is not yet supported."
187 );
188 }
189 let summed = counts_u64.sum_axis(ndarray::Axis(0));
190
191 let counts_f64: Array3<f64> = summed
193 .mapv(|v| v as f64)
194 .permuted_axes([2, 0, 1])
195 .as_standard_layout()
196 .into_owned();
197 let n_tof = counts_f64.shape()[0];
198
199 let tof_edges_us = read_tof_axis(&hist_group)?;
201
202 if tof_edges_us.len() != n_tof + 1 && tof_edges_us.len() != n_tof {
204 return Err(IoError::InvalidParameter(format!(
205 "TOF axis length {} is incompatible with {} histogram bins (expected {} or {})",
206 tof_edges_us.len(),
207 n_tof,
208 n_tof,
209 n_tof + 1
210 )));
211 }
212
213 let flight_path_m = read_f64_attr(&hist_group, "flight_path_m")
215 .or_else(|| read_f64_attr(&entry, "flight_path_m"));
216
217 let dead_pixels = read_dead_pixel_mask(&entry);
219
220 Ok(NexusHistogramData {
221 counts: counts_f64,
222 tof_edges_us,
223 flight_path_m,
224 dead_pixels,
225 n_rotation_angles: n_rot,
226 event_stats: None, })
228}
229
230#[derive(Debug, Clone)]
232pub struct EventBinningParams {
233 pub n_bins: usize,
235 pub tof_min_us: f64,
237 pub tof_max_us: f64,
239 pub height: usize,
241 pub width: usize,
243}
244
245pub fn load_nexus_events(
262 path: &Path,
263 params: &EventBinningParams,
264) -> Result<NexusHistogramData, IoError> {
265 if params.n_bins == 0 {
266 return Err(IoError::InvalidParameter("n_bins must be positive".into()));
267 }
268 if params.height == 0 || params.width == 0 {
269 return Err(IoError::InvalidParameter(
270 "height and width must be positive".into(),
271 ));
272 }
273 if !params.tof_min_us.is_finite() || !params.tof_max_us.is_finite() {
274 return Err(IoError::InvalidParameter(
275 "TOF bounds must be finite".into(),
276 ));
277 }
278 if params.tof_max_us <= params.tof_min_us {
279 return Err(IoError::InvalidParameter(format!(
280 "tof_max_us ({}) must be greater than tof_min_us ({})",
281 params.tof_max_us, params.tof_min_us
282 )));
283 }
284
285 let file = hdf5::File::open(path).map_err(|e| {
286 IoError::FileNotFound(
287 path.display().to_string(),
288 std::io::Error::other(e.to_string()),
289 )
290 })?;
291
292 let entry = file
293 .group("entry")
294 .map_err(|e| IoError::InvalidParameter(format!("Missing /entry group: {e}")))?;
295
296 let neutrons = entry
297 .group("neutrons")
298 .map_err(|e| IoError::InvalidParameter(format!("Missing /entry/neutrons group: {e}")))?;
299
300 let tof_ns: Vec<u64> = neutrons
302 .dataset("event_time_offset")
303 .map_err(|e| IoError::InvalidParameter(format!("Missing event_time_offset dataset: {e}")))?
304 .read_1d()
305 .map_err(|e| IoError::InvalidParameter(format!("Failed to read event_time_offset: {e}")))?
306 .to_vec();
307
308 let x_coords: Vec<f64> = neutrons
309 .dataset("x")
310 .map_err(|e| IoError::InvalidParameter(format!("Missing x dataset: {e}")))?
311 .read_1d()
312 .map_err(|e| IoError::InvalidParameter(format!("Failed to read x: {e}")))?
313 .to_vec();
314
315 let y_coords: Vec<f64> = neutrons
316 .dataset("y")
317 .map_err(|e| IoError::InvalidParameter(format!("Missing y dataset: {e}")))?
318 .read_1d()
319 .map_err(|e| IoError::InvalidParameter(format!("Failed to read y: {e}")))?
320 .to_vec();
321
322 if tof_ns.len() != x_coords.len() || tof_ns.len() != y_coords.len() {
323 return Err(IoError::ShapeMismatch(format!(
324 "Event arrays have mismatched lengths: tof={}, x={}, y={}",
325 tof_ns.len(),
326 x_coords.len(),
327 y_coords.len()
328 )));
329 }
330
331 let tof_edges_us =
333 crate::tof::linspace_tof_edges(params.tof_min_us, params.tof_max_us, params.n_bins)?;
334
335 let dt_us = (params.tof_max_us - params.tof_min_us) / params.n_bins as f64;
337 let mut counts = Array3::<f64>::zeros((params.n_bins, params.height, params.width));
338 let total = tof_ns.len();
339 let mut kept = 0usize;
340 let mut dropped_non_finite = 0usize;
341 let mut dropped_tof_range = 0usize;
342 let mut dropped_spatial = 0usize;
343
344 for i in 0..tof_ns.len() {
345 let tof_us = tof_ns[i] as f64 / 1000.0; if !tof_us.is_finite() {
347 dropped_non_finite += 1;
348 continue;
349 }
350
351 if tof_us < params.tof_min_us || tof_us >= params.tof_max_us {
352 dropped_tof_range += 1;
353 continue;
354 }
355
356 let xf = x_coords[i];
357 let yf = y_coords[i];
358 if !xf.is_finite() || !yf.is_finite() {
359 dropped_non_finite += 1;
360 continue;
361 }
362 let px = xf.round() as isize;
363 let py = yf.round() as isize;
364
365 if px < 0 || py < 0 || px >= params.width as isize || py >= params.height as isize {
366 dropped_spatial += 1;
367 continue;
368 }
369
370 let tof_bin = ((tof_us - params.tof_min_us) / dt_us) as usize;
371 let tof_bin = tof_bin.min(params.n_bins - 1);
372 counts[[tof_bin, py as usize, px as usize]] += 1.0;
373 kept += 1;
374 }
375
376 let flight_path_m = read_f64_attr(&neutrons, "flight_path_m")
378 .or_else(|| read_f64_attr(&entry, "flight_path_m"));
379
380 let dead_pixels = read_dead_pixel_mask(&entry);
382
383 debug_assert_eq!(
384 total,
385 kept + dropped_non_finite + dropped_tof_range + dropped_spatial,
386 "event retention accounting mismatch"
387 );
388
389 Ok(NexusHistogramData {
390 counts,
391 tof_edges_us,
392 flight_path_m,
393 dead_pixels,
394 n_rotation_angles: 1,
395 event_stats: Some(EventRetentionStats {
396 total,
397 kept,
398 dropped_non_finite,
399 dropped_tof_range,
400 dropped_spatial,
401 }),
402 })
403}
404
405fn probe_histogram_group(entry: &hdf5::Group) -> (bool, Option<[usize; 4]>, Option<Vec<f64>>) {
409 let hist = match entry.group("histogram") {
410 Ok(g) => g,
411 Err(_) => return (false, None, None),
412 };
413
414 let counts = match hist.dataset("counts") {
415 Ok(ds) => ds,
416 Err(_) => return (false, None, None),
417 };
418
419 let shape = counts.shape();
420 if shape.len() != 4 {
421 return (false, None, None);
422 }
423
424 let histogram_shape = Some([shape[0], shape[1], shape[2], shape[3]]);
425
426 let tof_edges = hist
428 .dataset("time_of_flight")
429 .ok()
430 .and_then(|ds| ds.read_1d::<f64>().ok())
431 .map(|a| a.to_vec());
432
433 (true, histogram_shape, tof_edges)
434}
435
436fn probe_event_group(entry: &hdf5::Group) -> (bool, Option<usize>) {
438 let neutrons = match entry.group("neutrons") {
439 Ok(g) => g,
440 Err(_) => return (false, None),
441 };
442
443 let n_events = neutrons
444 .dataset("event_time_offset")
445 .ok()
446 .map(|ds| ds.shape().first().copied().unwrap_or(0));
447
448 (n_events.is_some(), n_events)
449}
450
451fn read_tof_axis(hist_group: &hdf5::Group) -> Result<Vec<f64>, IoError> {
453 let tof_ds = hist_group.dataset("time_of_flight").map_err(|e| {
454 IoError::InvalidParameter(format!(
455 "Missing /entry/histogram/time_of_flight dataset: {e}"
456 ))
457 })?;
458
459 let tof_ns: Vec<f64> = tof_ds
460 .read_1d::<f64>()
461 .map_err(|e| IoError::InvalidParameter(format!("Failed to read time_of_flight: {e}")))?
462 .to_vec();
463
464 Ok(tof_ns.iter().map(|&ns| ns / 1000.0).collect())
466}
467
468fn read_f64_attr(group: &hdf5::Group, name: &str) -> Option<f64> {
470 group
471 .attr(name)
472 .ok()
473 .and_then(|a| a.read_scalar::<f64>().ok())
474}
475
476fn read_dead_pixel_mask(entry: &hdf5::Group) -> Option<ndarray::Array2<bool>> {
478 let masks = entry.group("pixel_masks").ok()?;
479 let dead_ds = masks.dataset("dead").ok()?;
480 let dead_u8: ndarray::Array2<u8> = dead_ds.read().ok()?;
481 Some(dead_u8.mapv(|v| v != 0))
482}
483
484pub fn list_hdf5_tree(path: &Path, max_depth: usize) -> Result<Vec<Hdf5TreeEntry>, IoError> {
490 let file = hdf5::File::open(path)
491 .map_err(|e| IoError::Hdf5Error(format!("Cannot open HDF5 file: {e}")))?;
492 let mut entries = Vec::new();
493 walk_group(
494 &file
495 .as_group()
496 .map_err(|e| IoError::Hdf5Error(format!("Cannot read root group: {e}")))?,
497 "/",
498 0,
499 max_depth,
500 &mut entries,
501 );
502 Ok(entries)
503}
504
505fn walk_group(
507 group: &hdf5::Group,
508 prefix: &str,
509 depth: usize,
510 max_depth: usize,
511 entries: &mut Vec<Hdf5TreeEntry>,
512) {
513 let Ok(members) = group.member_names() else {
514 return;
515 };
516 let mut members = members;
517 members.sort();
518 for name in &members {
519 let child_path = if prefix == "/" {
520 format!("/{name}")
521 } else {
522 format!("{prefix}/{name}")
523 };
524
525 if let Ok(ds) = group.dataset(name) {
527 let shape = ds.shape();
528 entries.push(Hdf5TreeEntry {
529 path: child_path,
530 kind: Hdf5EntryKind::Dataset,
531 shape: Some(shape),
532 });
533 } else if let Ok(child_group) = group.group(name) {
534 entries.push(Hdf5TreeEntry {
536 path: child_path.clone(),
537 kind: Hdf5EntryKind::Group,
538 shape: None,
539 });
540 if depth < max_depth {
541 walk_group(&child_group, &child_path, depth + 1, max_depth, entries);
542 }
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn create_test_histogram(
553 path: &Path,
554 counts: &[u64],
555 shape: [usize; 4],
556 tof_ns: &[f64],
557 flight_path_m: Option<f64>,
558 ) {
559 let file = hdf5::File::create(path).expect("create test file");
560 let entry = file.create_group("entry").expect("create entry");
561
562 if let Some(fp) = flight_path_m {
563 entry
564 .new_attr::<f64>()
565 .shape(())
566 .create("flight_path_m")
567 .expect("create attr")
568 .write_scalar(&fp)
569 .expect("write attr");
570 }
571
572 let hist = entry.create_group("histogram").expect("create histogram");
573 hist.new_dataset::<u64>()
574 .shape(shape)
575 .create("counts")
576 .expect("create counts")
577 .write_raw(counts)
578 .expect("write counts");
579
580 hist.new_dataset::<f64>()
581 .shape([tof_ns.len()])
582 .create("time_of_flight")
583 .expect("create tof")
584 .write_raw(tof_ns)
585 .expect("write tof");
586 }
587
588 #[test]
589 fn test_probe_nexus_histogram() {
590 let dir = tempfile::tempdir().unwrap();
591 let path = dir.path().join("test.h5");
592
593 let counts = vec![0u64; 24];
595 let tof_ns = vec![1000.0, 2000.0, 3000.0, 4000.0, 5000.0]; create_test_histogram(&path, &counts, [1, 2, 3, 4], &tof_ns, Some(25.0));
597
598 let meta = probe_nexus(&path).unwrap();
599 assert!(meta.has_histogram);
600 assert!(!meta.has_events);
601 assert_eq!(meta.histogram_shape, Some([1, 2, 3, 4]));
602 assert_eq!(meta.flight_path_m, Some(25.0));
603 assert!(meta.tof_edges_ns.is_some());
604 assert_eq!(meta.tof_edges_ns.unwrap().len(), 5);
605 }
606
607 #[test]
608 fn test_load_nexus_histogram() {
609 let dir = tempfile::tempdir().unwrap();
610 let path = dir.path().join("test.h5");
611
612 let mut counts = vec![0u64; 2 * 2 * 3 * 2];
615 counts[0] = 10;
618 counts[12] = 5;
620
621 let tof_ns = vec![1000.0, 2000.0, 3000.0]; create_test_histogram(&path, &counts, [2, 2, 3, 2], &tof_ns, Some(25.0));
623
624 let data = load_nexus_histogram(&path).unwrap();
625
626 assert_eq!(data.counts.shape(), &[2, 2, 3]);
628
629 assert_eq!(data.counts[[0, 0, 0]], 15.0);
631
632 assert_eq!(data.tof_edges_us.len(), 3);
634 assert!((data.tof_edges_us[0] - 1.0).abs() < 1e-10); assert!((data.tof_edges_us[1] - 2.0).abs() < 1e-10);
636 assert!((data.tof_edges_us[2] - 3.0).abs() < 1e-10);
637
638 assert_eq!(data.flight_path_m, Some(25.0));
639
640 assert_eq!(data.n_rotation_angles, 2);
642 }
643
644 #[test]
645 fn test_ns_to_us_conversion() {
646 let dir = tempfile::tempdir().unwrap();
647 let path = dir.path().join("test.h5");
648
649 let counts = vec![0u64; 3];
650 let tof_ns = vec![500_000.0, 1_000_000.0, 1_500_000.0, 2_000_000.0];
651 create_test_histogram(&path, &counts, [1, 1, 1, 3], &tof_ns, None);
652
653 let data = load_nexus_histogram(&path).unwrap();
654
655 assert!((data.tof_edges_us[0] - 500.0).abs() < 1e-10);
657 assert!((data.tof_edges_us[1] - 1000.0).abs() < 1e-10);
658 assert!((data.tof_edges_us[2] - 1500.0).abs() < 1e-10);
659 assert!((data.tof_edges_us[3] - 2000.0).abs() < 1e-10);
660 }
661
662 #[test]
663 fn test_probe_missing_dataset() {
664 let dir = tempfile::tempdir().unwrap();
665 let path = dir.path().join("empty.h5");
666
667 let file = hdf5::File::create(&path).expect("create");
668 file.create_group("entry").expect("create entry");
669 drop(file);
670
671 let meta = probe_nexus(&path).unwrap();
672 assert!(!meta.has_histogram);
673 assert!(!meta.has_events);
674 assert!(meta.histogram_shape.is_none());
675 assert!(meta.n_events.is_none());
676 }
677
678 fn create_test_events(
680 path: &Path,
681 tof_ns: &[u64],
682 x: &[f64],
683 y: &[f64],
684 flight_path_m: Option<f64>,
685 ) {
686 let file = hdf5::File::create(path).expect("create");
687 let entry = file.create_group("entry").expect("create entry");
688
689 if let Some(fp) = flight_path_m {
690 entry
691 .new_attr::<f64>()
692 .shape(())
693 .create("flight_path_m")
694 .expect("create attr")
695 .write_scalar(&fp)
696 .expect("write attr");
697 }
698
699 let neutrons = entry.create_group("neutrons").expect("create neutrons");
700 neutrons
701 .new_dataset::<u64>()
702 .shape([tof_ns.len()])
703 .create("event_time_offset")
704 .expect("create tof")
705 .write_raw(tof_ns)
706 .expect("write tof");
707 neutrons
708 .new_dataset::<f64>()
709 .shape([x.len()])
710 .create("x")
711 .expect("create x")
712 .write_raw(x)
713 .expect("write x");
714 neutrons
715 .new_dataset::<f64>()
716 .shape([y.len()])
717 .create("y")
718 .expect("create y")
719 .write_raw(y)
720 .expect("write y");
721 }
722
723 #[test]
724 fn test_histogram_known_events() {
725 let dir = tempfile::tempdir().unwrap();
726 let path = dir.path().join("events.h5");
727
728 let tof_ns = vec![1_500_000, 2_500_000, 1_800_000];
730 let x = vec![1.0, 1.0, 1.0];
731 let y = vec![0.0, 0.0, 0.0];
732 create_test_events(&path, &tof_ns, &x, &y, Some(25.0));
733
734 let params = EventBinningParams {
735 n_bins: 2,
736 tof_min_us: 1000.0,
737 tof_max_us: 3000.0,
738 height: 2,
739 width: 3,
740 };
741
742 let data = load_nexus_events(&path, ¶ms).unwrap();
743 assert_eq!(data.counts.shape(), &[2, 2, 3]);
744
745 assert_eq!(data.counts[[0, 0, 1]], 2.0);
747 assert_eq!(data.counts[[1, 0, 1]], 1.0);
749
750 assert_eq!(data.flight_path_m, Some(25.0));
751 assert_eq!(data.tof_edges_us.len(), 3); let stats = data
755 .event_stats
756 .as_ref()
757 .expect("event_stats should be Some");
758 assert_eq!(stats.total, 3);
759 assert_eq!(stats.kept, 3);
760 assert_eq!(stats.dropped_non_finite, 0);
761 assert_eq!(stats.dropped_tof_range, 0);
762 assert_eq!(stats.dropped_spatial, 0);
763 }
764
765 #[test]
766 fn test_filter_out_of_range_events() {
767 let dir = tempfile::tempdir().unwrap();
768 let path = dir.path().join("events_oob.h5");
769
770 let tof_ns = vec![
772 1_500_000, 500_000, 1_500_000, ];
776 let x = vec![0.0, 0.0, 5.0]; let y = vec![0.0, 0.0, 0.0];
778 create_test_events(&path, &tof_ns, &x, &y, None);
779
780 let params = EventBinningParams {
781 n_bins: 2,
782 tof_min_us: 1000.0,
783 tof_max_us: 3000.0,
784 height: 2,
785 width: 3,
786 };
787
788 let data = load_nexus_events(&path, ¶ms).unwrap();
789
790 let total: f64 = data.counts.iter().sum();
792 assert_eq!(total, 1.0);
793 assert_eq!(data.counts[[0, 0, 0]], 1.0);
794
795 let stats = data
797 .event_stats
798 .as_ref()
799 .expect("event_stats should be Some");
800 assert_eq!(stats.total, 3);
801 assert_eq!(stats.kept, 1);
802 assert_eq!(stats.dropped_non_finite, 0);
803 assert_eq!(stats.dropped_tof_range, 1);
804 assert_eq!(stats.dropped_spatial, 1);
805 }
806
807 #[test]
808 fn test_empty_events() {
809 let dir = tempfile::tempdir().unwrap();
810 let path = dir.path().join("empty_events.h5");
811
812 create_test_events(&path, &[], &[], &[], None);
813
814 let params = EventBinningParams {
815 n_bins: 10,
816 tof_min_us: 1000.0,
817 tof_max_us: 20000.0,
818 height: 4,
819 width: 4,
820 };
821
822 let data = load_nexus_events(&path, ¶ms).unwrap();
823 assert_eq!(data.counts.shape(), &[10, 4, 4]);
824
825 let total: f64 = data.counts.iter().sum();
826 assert_eq!(total, 0.0);
827
828 let stats = data
830 .event_stats
831 .as_ref()
832 .expect("event_stats should be Some");
833 assert_eq!(stats.total, 0);
834 assert_eq!(stats.kept, 0);
835 assert_eq!(stats.dropped_non_finite, 0);
836 assert_eq!(stats.dropped_tof_range, 0);
837 assert_eq!(stats.dropped_spatial, 0);
838 }
839
840 #[test]
841 fn test_probe_with_events() {
842 let dir = tempfile::tempdir().unwrap();
843 let path = dir.path().join("with_events.h5");
844
845 create_test_events(
846 &path,
847 &[1000, 2000, 3000],
848 &[0.0, 1.0, 2.0],
849 &[0.0, 0.0, 1.0],
850 None,
851 );
852
853 let meta = probe_nexus(&path).unwrap();
854 assert!(!meta.has_histogram);
855 assert!(meta.has_events);
856 assert_eq!(meta.n_events, Some(3));
857 }
858
859 #[test]
860 fn test_list_hdf5_tree() {
861 let dir = tempfile::tempdir().unwrap();
862 let path = dir.path().join("tree.h5");
863
864 {
866 let file = hdf5::File::create(&path).expect("create file");
867 let g1 = file.create_group("entry").expect("create entry");
868 let g2 = g1.create_group("histogram").expect("create histogram");
869 g2.new_dataset::<f64>()
870 .shape([3])
871 .create("data")
872 .expect("create data")
873 .write_raw(&[1.0, 2.0, 3.0])
874 .expect("write data");
875 }
876
877 let tree = list_hdf5_tree(&path, 10).unwrap();
878 assert!(!tree.is_empty());
879
880 let paths: Vec<&str> = tree.iter().map(|e| e.path.as_str()).collect();
882 assert!(paths.contains(&"/entry"));
883 assert!(paths.contains(&"/entry/histogram"));
884 assert!(paths.contains(&"/entry/histogram/data"));
885
886 let data_entry = tree
888 .iter()
889 .find(|e| e.path == "/entry/histogram/data")
890 .unwrap();
891 assert!(data_entry.shape.is_some());
892 }
893
894 #[test]
895 fn test_nan_xy_coords_dropped() {
896 let dir = tempfile::tempdir().unwrap();
897 let path = dir.path().join("nan_xy.h5");
898
899 let tof_ns = vec![1_500_000, 1_500_000, 1_500_000, 2_500_000];
901 let x = vec![0.0, f64::NAN, 0.0, 1.0];
902 let y = vec![0.0, 0.0, f64::INFINITY, 0.0];
903 create_test_events(&path, &tof_ns, &x, &y, None);
904
905 let params = EventBinningParams {
906 n_bins: 2,
907 tof_min_us: 1000.0,
908 tof_max_us: 3000.0,
909 height: 2,
910 width: 3,
911 };
912
913 let data = load_nexus_events(&path, ¶ms).unwrap();
914
915 let total_counts: f64 = data.counts.iter().sum();
917 assert_eq!(total_counts, 2.0);
918
919 let stats = data
920 .event_stats
921 .as_ref()
922 .expect("event_stats should be Some");
923 assert_eq!(stats.total, 4);
924 assert_eq!(stats.kept, 2);
925 assert_eq!(stats.dropped_non_finite, 2);
926 assert_eq!(stats.dropped_tof_range, 0);
927 assert_eq!(stats.dropped_spatial, 0);
928 }
929}