1use std::path::{Path, PathBuf};
16
17use ad_core_rs::error::{ADError, ADResult};
18use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
19use ad_core_rs::ndarray_pool::NDArrayPool;
20use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
21use ad_core_rs::plugin::file_controller::FilePluginController;
22use ad_core_rs::plugin::runtime::{
23 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
24};
25use parking_lot::Mutex;
26
27use rust_hdf5::{H5Dataset, H5File};
28
29const DTYPE_ATTR: &str = "NDArrayDataType";
34
35fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
41 match buf {
42 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
43 NDDataBuffer::U8(v) => v.clone(),
44 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
45 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
46 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
47 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
48 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
49 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
50 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
51 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
52 }
53}
54
55#[derive(Debug, Clone)]
62pub struct XmlElement {
63 pub name: String,
64 pub attrs: Vec<(String, String)>,
65 pub children: Vec<XmlElement>,
66 pub text: String,
68}
69
70impl XmlElement {
71 fn attr(&self, key: &str) -> Option<&str> {
72 self.attrs
73 .iter()
74 .find(|(k, _)| k == key)
75 .map(|(_, v)| v.as_str())
76 }
77}
78
79#[derive(Debug)]
81pub struct NexusTemplateError(pub String);
82
83impl std::fmt::Display for NexusTemplateError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 write!(f, "NeXus template error: {}", self.0)
86 }
87}
88
89pub fn parse_nexus_template(text: &str) -> Result<XmlElement, NexusTemplateError> {
95 let chars: Vec<char> = text.chars().collect();
96 let mut pos = 0;
97 skip_prolog_and_ws(&chars, &mut pos);
98 let root = parse_element(&chars, &mut pos)?;
99 Ok(root)
100}
101
102fn skip_prolog_and_ws(chars: &[char], pos: &mut usize) {
103 loop {
104 while *pos < chars.len() && chars[*pos].is_whitespace() {
105 *pos += 1;
106 }
107 if chars[*pos..].starts_with(&['<', '?']) {
108 while *pos < chars.len() && !(chars[*pos] == '?' && chars.get(*pos + 1) == Some(&'>')) {
110 *pos += 1;
111 }
112 *pos += 2;
113 } else if chars[*pos..].starts_with(&['<', '!', '-', '-']) {
114 skip_comment(chars, pos);
115 } else {
116 break;
117 }
118 }
119}
120
121fn skip_comment(chars: &[char], pos: &mut usize) {
122 *pos += 4;
124 while *pos < chars.len()
125 && !(chars[*pos] == '-'
126 && chars.get(*pos + 1) == Some(&'-')
127 && chars.get(*pos + 2) == Some(&'>'))
128 {
129 *pos += 1;
130 }
131 *pos += 3;
132}
133
134fn parse_element(chars: &[char], pos: &mut usize) -> Result<XmlElement, NexusTemplateError> {
135 if *pos >= chars.len() || chars[*pos] != '<' {
136 return Err(NexusTemplateError("expected element start '<'".into()));
137 }
138 *pos += 1;
139 let name = read_name(chars, pos);
141 if name.is_empty() {
142 return Err(NexusTemplateError("empty element name".into()));
143 }
144 let mut attrs = Vec::new();
146 loop {
147 skip_ws(chars, pos);
148 if *pos >= chars.len() {
149 return Err(NexusTemplateError("unterminated tag".into()));
150 }
151 if chars[*pos] == '/' && chars.get(*pos + 1) == Some(&'>') {
152 *pos += 2;
153 return Ok(XmlElement {
154 name,
155 attrs,
156 children: Vec::new(),
157 text: String::new(),
158 });
159 }
160 if chars[*pos] == '>' {
161 *pos += 1;
162 break;
163 }
164 let attr_name = read_name(chars, pos);
165 if attr_name.is_empty() {
166 return Err(NexusTemplateError(format!(
167 "malformed attribute in tag '{}'",
168 name
169 )));
170 }
171 skip_ws(chars, pos);
172 if *pos >= chars.len() || chars[*pos] != '=' {
173 return Err(NexusTemplateError(format!(
174 "attribute '{}' missing '='",
175 attr_name
176 )));
177 }
178 *pos += 1;
179 skip_ws(chars, pos);
180 let value = read_quoted(chars, pos)?;
181 attrs.push((attr_name, value));
182 }
183 let mut children = Vec::new();
185 let mut text = String::new();
186 loop {
187 if *pos >= chars.len() {
188 return Err(NexusTemplateError(format!("unclosed element '{}'", name)));
189 }
190 if chars[*pos..].starts_with(&['<', '!', '-', '-']) {
191 skip_comment(chars, pos);
192 continue;
193 }
194 if chars[*pos] == '<' && chars.get(*pos + 1) == Some(&'/') {
195 *pos += 2;
197 let close_name = read_name(chars, pos);
198 skip_ws(chars, pos);
199 if *pos < chars.len() && chars[*pos] == '>' {
200 *pos += 1;
201 }
202 if close_name != name {
203 return Err(NexusTemplateError(format!(
204 "mismatched close tag: expected '{}', got '{}'",
205 name, close_name
206 )));
207 }
208 break;
209 }
210 if chars[*pos] == '<' {
211 children.push(parse_element(chars, pos)?);
212 } else {
213 text.push(chars[*pos]);
214 *pos += 1;
215 }
216 }
217 Ok(XmlElement {
218 name,
219 attrs,
220 children,
221 text: text.trim().to_string(),
222 })
223}
224
225fn skip_ws(chars: &[char], pos: &mut usize) {
226 while *pos < chars.len() && chars[*pos].is_whitespace() {
227 *pos += 1;
228 }
229}
230
231fn read_name(chars: &[char], pos: &mut usize) -> String {
232 skip_ws(chars, pos);
233 let start = *pos;
234 while *pos < chars.len()
235 && !chars[*pos].is_whitespace()
236 && !matches!(chars[*pos], '=' | '>' | '/' | '<')
237 {
238 *pos += 1;
239 }
240 chars[start..*pos].iter().collect()
241}
242
243fn read_quoted(chars: &[char], pos: &mut usize) -> Result<String, NexusTemplateError> {
244 if *pos >= chars.len() || (chars[*pos] != '"' && chars[*pos] != '\'') {
245 return Err(NexusTemplateError("expected quoted attribute value".into()));
246 }
247 let quote = chars[*pos];
248 *pos += 1;
249 let start = *pos;
250 while *pos < chars.len() && chars[*pos] != quote {
251 *pos += 1;
252 }
253 if *pos >= chars.len() {
254 return Err(NexusTemplateError("unterminated attribute value".into()));
255 }
256 let value: String = chars[start..*pos].iter().collect();
257 *pos += 1;
258 Ok(value)
259}
260
261const NEXUS_GROUP_CLASSES: &[&str] = &[
265 "NXentry",
266 "NXinstrument",
267 "NXsample",
268 "NXmonitor",
269 "NXsource",
270 "NXuser",
271 "NXdata",
272 "NXdetector",
273 "NXaperature",
274 "NXattenuator",
275 "NXbeam_stop",
276 "NXbending_magnet",
277 "NXcollimator",
278 "NXcrystal",
279 "NXdisk_chopper",
280 "NXfermi_chopper",
281 "NXfilter",
282 "NXflipper",
283 "NXguide",
284 "NXinsertion_device",
285 "NXmirror",
286 "NXmoderator",
287 "NXmonochromator",
288 "NXpolarizer",
289 "NXpositioner",
290 "NXvelocity_selector",
291 "NXevent_data",
292 "NXprocess",
293 "NXcharacterization",
294 "NXlog",
295 "NXnote",
296 "NXbeam",
297 "NXgeometry",
298 "NXtranslation",
299 "NXshape",
300 "NXorientation",
301 "NXenvironment",
302 "NXsensor",
303 "NXcapillary",
304 "NXcollection",
305 "NXdetector_group",
306 "NXparameters",
307 "NXsubentry",
308 "NXxraylens",
309];
310
311fn is_nexus_group(el: &XmlElement) -> bool {
313 NEXUS_GROUP_CLASSES.contains(&el.name.as_str()) || el.attr("type") == Some("UserGroup")
314}
315
316pub struct NexusWriter {
318 current_path: Option<PathBuf>,
319 file: Option<H5File>,
320 frame_count: usize,
321 dataset: Option<H5Dataset>,
323 uid_dataset: Option<H5Dataset>,
325 ts_dataset: Option<H5Dataset>,
327 template: Option<XmlElement>,
329 data_group_path: String,
332 data_node_name: String,
333 nxdata_group_path: Option<String>,
337}
338
339impl NexusWriter {
340 pub fn new() -> Self {
341 Self {
342 current_path: None,
343 file: None,
344 frame_count: 0,
345 dataset: None,
346 uid_dataset: None,
347 ts_dataset: None,
348 template: None,
349 data_group_path: "entry/instrument/detector".to_string(),
350 data_node_name: "data".to_string(),
351 nxdata_group_path: None,
352 }
353 }
354
355 pub fn frame_count(&self) -> usize {
356 self.frame_count
357 }
358
359 pub fn has_template(&self) -> bool {
361 self.template.is_some()
362 }
363
364 pub fn load_template(&mut self, xml: &str) -> bool {
369 match parse_nexus_template(xml) {
370 Ok(root) => {
371 self.template = Some(root);
372 true
373 }
374 Err(_) => {
375 self.template = None;
376 false
377 }
378 }
379 }
380
381 pub fn clear_template(&mut self) {
383 self.template = None;
384 }
385
386 fn write_nx_class(group: &rust_hdf5::H5Group, class_name: &str) -> ADResult<()> {
393 group.set_attr_string("NX_class", class_name).map_err(|e| {
394 ADError::UnsupportedConversion(format!("NX_class group attr error: {}", e))
395 })
396 }
397
398 fn apply_root_nx_class(file: &H5File, class_name: &str) {
400 let _ = file.set_attr_string("NX_class", class_name);
401 }
402
403 fn process_template(
411 h5file: &H5File,
412 template: &XmlElement,
413 array: &NDArray,
414 ) -> ADResult<(String, String)> {
415 let mut data_group = String::new();
416 let mut data_node = String::new();
417 let top_children: &[XmlElement] = if template.name == "NXroot" {
419 &template.children
420 } else {
421 std::slice::from_ref(template)
422 };
423 for child in top_children {
424 Self::process_node(
425 h5file,
426 None,
427 "",
428 child,
429 array,
430 &mut data_group,
431 &mut data_node,
432 )?;
433 }
434 if data_node.is_empty() {
435 Ok(("entry/instrument/detector".to_string(), "data".to_string()))
436 } else {
437 Ok((data_group, data_node))
438 }
439 }
440
441 fn process_node(
444 h5file: &H5File,
445 parent: Option<&rust_hdf5::H5Group>,
446 parent_path: &str,
447 node: &XmlElement,
448 array: &NDArray,
449 data_group: &mut String,
450 data_node: &mut String,
451 ) -> ADResult<()> {
452 let node_type = node.attr("type").map(|s| s.to_string());
453
454 if is_nexus_group(node) {
455 let group_name = node.attr("name").unwrap_or(&node.name).to_string();
458 let group = match parent {
459 Some(p) => p.create_group(&group_name),
460 None => h5file.create_group(&group_name),
461 }
462 .map_err(|e| {
463 ADError::UnsupportedConversion(format!("NeXus group '{}': {}", group_name, e))
464 })?;
465 let class_name = if NEXUS_GROUP_CLASSES.contains(&node.name.as_str()) {
466 node.name.clone()
467 } else {
468 node.attr("type").unwrap_or("NXcollection").to_string()
470 };
471 Self::write_nx_class(&group, &class_name)?;
472 let child_path = if parent_path.is_empty() {
473 group_name.clone()
474 } else {
475 format!("{}/{}", parent_path, group_name)
476 };
477 for child in &node.children {
478 Self::process_node(
479 h5file,
480 Some(&group),
481 &child_path,
482 child,
483 array,
484 data_group,
485 data_node,
486 )?;
487 }
488 return Ok(());
489 }
490
491 match node_type.as_deref() {
493 Some("pArray") => {
494 *data_group = parent_path.to_string();
497 *data_node = node.name.clone();
498 }
499 Some("CONST") => {
500 if let Some(parent) = parent {
502 Self::write_const_dataset(parent, &node.name, &node.text)?;
503 }
504 }
505 Some("ND_ATTR") => {
506 if let Some(parent) = parent {
508 let source = node.attr("source").unwrap_or(&node.name);
509 if let Some(attr) = array.attributes.get(source) {
510 Self::write_attr_dataset(parent, &node.name, &attr.value)?;
511 }
512 }
513 }
514 Some("Attr") | None if node.name == "Attr" => {
515 if let Some(parent) = parent {
518 let attr_name = node.attr("name").unwrap_or(&node.name);
519 let value = if node.attr("type") == Some("ND_ATTR") {
520 node.attr("source")
521 .and_then(|s| array.attributes.get(s))
522 .map(|a| a.value.as_string())
523 .unwrap_or_default()
524 } else {
525 node.text.clone()
526 };
527 parent.set_attr_string(attr_name, &value).map_err(|e| {
528 ADError::UnsupportedConversion(format!("NeXus group attr error: {}", e))
529 })?;
530 }
531 }
532 _ => {
533 if let Some(parent) = parent {
535 let text = if node.text.is_empty() {
536 "LEFT BLANK"
537 } else {
538 &node.text
539 };
540 Self::write_const_dataset(parent, &node.name, text)?;
541 }
542 }
543 }
544 Ok(())
545 }
546
547 fn write_const_dataset(group: &rust_hdf5::H5Group, name: &str, text: &str) -> ADResult<()> {
549 let bytes = text.as_bytes();
550 let len = bytes.len().max(1);
551 let ds = group
552 .new_dataset::<u8>()
553 .shape([len])
554 .create(name)
555 .map_err(|e| {
556 ADError::UnsupportedConversion(format!("NeXus const dataset '{}': {}", name, e))
557 })?;
558 let mut buf = bytes.to_vec();
559 if buf.is_empty() {
560 buf.push(0);
561 }
562 ds.write_raw(&buf).map_err(|e| {
563 ADError::UnsupportedConversion(format!("NeXus const write '{}': {}", name, e))
564 })?;
565 Ok(())
566 }
567
568 fn write_attr_dataset(
571 group: &rust_hdf5::H5Group,
572 name: &str,
573 value: &ad_core_rs::attributes::NDAttrValue,
574 ) -> ADResult<()> {
575 use ad_core_rs::attributes::NDAttrValue;
576 macro_rules! scalar {
577 ($t:ty, $v:expr) => {{
578 let ds = group
579 .new_dataset::<$t>()
580 .shape([1usize])
581 .create(name)
582 .map_err(|e| {
583 ADError::UnsupportedConversion(format!(
584 "NeXus attr dataset '{}': {}",
585 name, e
586 ))
587 })?;
588 ds.write_raw(&[$v]).map_err(|e| {
589 ADError::UnsupportedConversion(format!("NeXus attr write '{}': {}", name, e))
590 })?;
591 }};
592 }
593 match value {
594 NDAttrValue::Int8(v) => scalar!(i8, *v),
595 NDAttrValue::UInt8(v) => scalar!(u8, *v),
596 NDAttrValue::Int16(v) => scalar!(i16, *v),
597 NDAttrValue::UInt16(v) => scalar!(u16, *v),
598 NDAttrValue::Int32(v) => scalar!(i32, *v),
599 NDAttrValue::UInt32(v) => scalar!(u32, *v),
600 NDAttrValue::Int64(v) => scalar!(i64, *v),
601 NDAttrValue::UInt64(v) => scalar!(u64, *v),
602 NDAttrValue::Float32(v) => scalar!(f32, *v),
603 NDAttrValue::Float64(v) => scalar!(f64, *v),
604 NDAttrValue::String(s) => Self::write_const_dataset(group, name, s)?,
605 NDAttrValue::Undefined => Self::write_const_dataset(group, name, "")?,
606 }
607 Ok(())
608 }
609}
610
611impl Default for NexusWriter {
612 fn default() -> Self {
613 Self::new()
614 }
615}
616
617impl NDFileWriter for NexusWriter {
618 fn open_file(&mut self, path: &Path, _mode: NDFileMode, array: &NDArray) -> ADResult<()> {
619 self.current_path = Some(path.to_path_buf());
620 self.frame_count = 0;
621 self.dataset = None;
622 self.uid_dataset = None;
623 self.ts_dataset = None;
624
625 let h5file = H5File::create(path)
626 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus create error: {}", e)))?;
627
628 Self::apply_root_nx_class(&h5file, "NXroot");
630
631 self.dataset = None;
632 self.uid_dataset = None;
633 self.ts_dataset = None;
634 self.nxdata_group_path = None;
635
636 if let Some(template) = self.template.clone() {
637 let (data_group, data_node) = Self::process_template(&h5file, &template, array)?;
640 self.data_group_path = data_group;
641 self.data_node_name = data_node;
642 } else {
643 let entry = h5file
646 .create_group("entry")
647 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
648 Self::write_nx_class(&entry, "NXentry")?;
649 let instrument = entry
650 .create_group("instrument")
651 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
652 Self::write_nx_class(&instrument, "NXinstrument")?;
653 let detector = instrument
654 .create_group("detector")
655 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
656 Self::write_nx_class(&detector, "NXdetector")?;
657 let data_group = entry
658 .create_group("data")
659 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
660 Self::write_nx_class(&data_group, "NXdata")?;
661 self.data_group_path = "entry/instrument/detector".to_string();
662 self.data_node_name = "data".to_string();
663 self.nxdata_group_path = Some("entry/data".to_string());
667 }
668
669 self.file = Some(h5file);
670 Ok(())
671 }
672
673 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
674 let h5file = self
675 .file
676 .as_ref()
677 .ok_or_else(|| ADError::UnsupportedConversion("no NeXus file open".into()))?;
678
679 let frame_shape = array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>();
680 let data_node_name = self.data_node_name.clone();
681 let nxdata_group_path = self.nxdata_group_path.clone();
682
683 let resolve_group = |path: &str| -> ADResult<rust_hdf5::H5Group> {
685 let mut group = h5file.root_group();
686 for component in path.split('/') {
687 if component.is_empty() {
688 continue;
689 }
690 group = group
691 .group(component)
692 .map_err(|e| ADError::UnsupportedConversion(e.to_string()))?;
693 }
694 Ok(group)
695 };
696 let data_group = resolve_group(&self.data_group_path)?;
697
698 let dtype_ordinal = array.data.data_type() as i32;
702 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
703
704 if self.frame_count == 0 {
705 let mut ds_shape = vec![1usize];
708 ds_shape.extend_from_slice(&frame_shape);
709 let chunk_dims = ds_shape.clone();
710 let mut image_max_shape: Vec<Option<usize>> = vec![None];
716 image_max_shape.extend(frame_shape.iter().map(|&d| Some(d)));
717
718 macro_rules! create_image_ds {
721 ($group:expr, $t:ty, $name:expr) => {{
722 $group
723 .new_dataset::<$t>()
724 .shape(&ds_shape[..])
725 .chunk(&chunk_dims[..])
726 .max_shape(&image_max_shape[..])
727 .create($name)
728 .map_err(|e| {
729 ADError::UnsupportedConversion(format!("NeXus dataset error: {}", e))
730 })?
731 }};
732 }
733 macro_rules! create_typed {
734 ($group:expr, $name:expr) => {{
735 match array.data.data_type() {
736 NDDataType::Int8 => create_image_ds!($group, i8, $name),
737 NDDataType::UInt8 => create_image_ds!($group, u8, $name),
738 NDDataType::Int16 => create_image_ds!($group, i16, $name),
739 NDDataType::UInt16 => create_image_ds!($group, u16, $name),
740 NDDataType::Int32 => create_image_ds!($group, i32, $name),
741 NDDataType::UInt32 => create_image_ds!($group, u32, $name),
742 NDDataType::Int64 => create_image_ds!($group, i64, $name),
743 NDDataType::UInt64 => create_image_ds!($group, u64, $name),
744 NDDataType::Float32 => create_image_ds!($group, f32, $name),
745 NDDataType::Float64 => create_image_ds!($group, f64, $name),
746 }
747 }};
748 }
749
750 let ds = create_typed!(data_group, &data_node_name);
751 ds.write_chunk(0, &frame_bytes)
752 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus write error: {}", e)))?;
753 let _ = ds
755 .new_attr::<i32>()
756 .shape(())
757 .create(DTYPE_ATTR)
758 .and_then(|a| a.write_numeric(&dtype_ordinal));
759 for attr in array.attributes.iter() {
761 let val_str = attr.value.as_string();
762 let _ = ds
763 .new_attr::<rust_hdf5::types::VarLenUnicode>()
764 .shape(())
765 .create(attr.name.as_str())
766 .and_then(|a| {
767 let s: rust_hdf5::types::VarLenUnicode =
768 val_str.parse().unwrap_or_default();
769 a.write_scalar(&s)
770 });
771 }
772 self.dataset = Some(ds);
773
774 if let Some(ref nxpath) = nxdata_group_path {
777 let nxdata_group = resolve_group(nxpath)?;
778 let target = format!("/{}/{}", self.data_group_path, data_node_name);
783 nxdata_group.link("data", &target).map_err(|e| {
784 ADError::UnsupportedConversion(format!("NeXus NXdata link error: {}", e))
785 })?;
786 }
787
788 let uid = data_group
792 .new_dataset::<i32>()
793 .shape([1usize])
794 .chunk(&[1usize])
795 .resizable()
796 .create("uniqueId")
797 .map_err(|e| {
798 ADError::UnsupportedConversion(format!("NeXus uniqueId dataset: {}", e))
799 })?;
800 uid.write_chunk(0, &array.unique_id.to_le_bytes())
801 .map_err(|e| {
802 ADError::UnsupportedConversion(format!("NeXus uniqueId write: {}", e))
803 })?;
804 self.uid_dataset = Some(uid);
805
806 let ts = data_group
807 .new_dataset::<f64>()
808 .shape([1usize])
809 .chunk(&[1usize])
810 .resizable()
811 .create("timeStamp")
812 .map_err(|e| {
813 ADError::UnsupportedConversion(format!("NeXus timeStamp dataset: {}", e))
814 })?;
815 ts.write_chunk(0, &array.time_stamp.to_le_bytes())
816 .map_err(|e| {
817 ADError::UnsupportedConversion(format!("NeXus timeStamp write: {}", e))
818 })?;
819 self.ts_dataset = Some(ts);
820 } else {
821 let ds = self.dataset.as_ref().ok_or_else(|| {
823 ADError::UnsupportedConversion("no dataset for multi-frame write".into())
824 })?;
825
826 let new_frame_count = self.frame_count + 1;
827 let mut new_shape = vec![new_frame_count];
828 new_shape.extend_from_slice(&frame_shape);
829 ds.extend(&new_shape).map_err(|e| {
830 ADError::UnsupportedConversion(format!("NeXus extend error: {}", e))
831 })?;
832 ds.write_chunk(self.frame_count, &frame_bytes)
833 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus write error: {}", e)))?;
834
835 if let Some(uid) = self.uid_dataset.as_ref() {
840 uid.extend(&[new_frame_count]).map_err(|e| {
841 ADError::UnsupportedConversion(format!("NeXus uniqueId extend: {}", e))
842 })?;
843 uid.write_chunk(self.frame_count, &array.unique_id.to_le_bytes())
844 .map_err(|e| {
845 ADError::UnsupportedConversion(format!("NeXus uniqueId write: {}", e))
846 })?;
847 }
848 if let Some(ts) = self.ts_dataset.as_ref() {
849 ts.extend(&[new_frame_count]).map_err(|e| {
850 ADError::UnsupportedConversion(format!("NeXus timeStamp extend: {}", e))
851 })?;
852 ts.write_chunk(self.frame_count, &array.time_stamp.to_le_bytes())
853 .map_err(|e| {
854 ADError::UnsupportedConversion(format!("NeXus timeStamp write: {}", e))
855 })?;
856 }
857 }
858
859 self.frame_count += 1;
860 Ok(())
861 }
862
863 fn read_file(&mut self) -> ADResult<NDArray> {
864 let path = self
865 .current_path
866 .as_ref()
867 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
868
869 let h5file = H5File::open(path)
870 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus open error: {}", e)))?;
871
872 let data_path = format!("{}/{}", self.data_group_path, self.data_node_name);
875 let ds = h5file
876 .dataset(&data_path)
877 .map_err(|e| ADError::UnsupportedConversion(format!("NeXus dataset error: {}", e)))?;
878
879 let shape = ds.shape();
880 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
881 let element_size = ds.element_size();
882
883 let recorded: Option<NDDataType> = ds
887 .attr(DTYPE_ATTR)
888 .ok()
889 .and_then(|a| a.read_numeric::<i32>().ok())
890 .and_then(|v| NDDataType::from_ordinal(v as u8));
891
892 let data_type = recorded.unwrap_or(match element_size {
893 1 => NDDataType::UInt8,
894 2 => NDDataType::UInt16,
895 4 => NDDataType::Float32,
896 8 => NDDataType::Float64,
897 other => {
898 return Err(ADError::UnsupportedConversion(format!(
899 "unsupported NeXus element size {}",
900 other
901 )));
902 }
903 });
904
905 macro_rules! read_typed {
906 ($t:ty, $variant:ident) => {{
907 let data = ds.read_raw::<$t>().map_err(|e| {
908 ADError::UnsupportedConversion(format!("NeXus read error: {}", e))
909 })?;
910 let mut arr = NDArray::new(dims, data_type);
911 arr.data = NDDataBuffer::$variant(data);
912 return Ok(arr);
913 }};
914 }
915
916 match data_type {
917 NDDataType::Int8 => read_typed!(i8, I8),
918 NDDataType::UInt8 => read_typed!(u8, U8),
919 NDDataType::Int16 => read_typed!(i16, I16),
920 NDDataType::UInt16 => read_typed!(u16, U16),
921 NDDataType::Int32 => read_typed!(i32, I32),
922 NDDataType::UInt32 => read_typed!(u32, U32),
923 NDDataType::Int64 => read_typed!(i64, I64),
924 NDDataType::UInt64 => read_typed!(u64, U64),
925 NDDataType::Float32 => read_typed!(f32, F32),
926 NDDataType::Float64 => read_typed!(f64, F64),
927 }
928 }
929
930 fn close_file(&mut self) -> ADResult<()> {
931 self.dataset = None;
932 self.uid_dataset = None;
933 self.ts_dataset = None;
934 self.file = None;
935 self.current_path = None;
936 Ok(())
937 }
938
939 fn supports_multiple_arrays(&self) -> bool {
940 true
941 }
942}
943
944pub struct NexusFileProcessor {
949 ctrl: Mutex<FilePluginController<NexusWriter>>,
950 template_path_idx: Option<usize>,
951 template_file_idx: Option<usize>,
952 template_valid_idx: Option<usize>,
953 template: Mutex<NexusTemplateSource>,
956}
957
958#[derive(Default)]
961struct NexusTemplateSource {
962 path: String,
963 file: String,
964}
965
966impl NexusFileProcessor {
967 pub fn new() -> Self {
968 Self {
969 ctrl: Mutex::new(FilePluginController::new(NexusWriter::new())),
970 template_path_idx: None,
971 template_file_idx: None,
972 template_valid_idx: None,
973 template: Mutex::new(NexusTemplateSource::default()),
974 }
975 }
976
977 fn reload_template(&self) -> i32 {
982 let full = {
983 let t = self.template.lock();
984 if t.file.is_empty() {
985 self.ctrl.lock().writer.clear_template();
986 return 0;
987 }
988 format!("{}{}", t.path, t.file)
989 };
990 match std::fs::read_to_string(&full) {
991 Ok(xml) => {
992 if self.ctrl.lock().writer.load_template(&xml) {
993 1
994 } else {
995 0
996 }
997 }
998 Err(_) => {
999 self.ctrl.lock().writer.clear_template();
1000 0
1001 }
1002 }
1003 }
1004}
1005
1006impl Default for NexusFileProcessor {
1007 fn default() -> Self {
1008 Self::new()
1009 }
1010}
1011
1012impl NDPluginProcess for NexusFileProcessor {
1013 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
1014 self.ctrl.lock().process_array(array)
1015 }
1016
1017 fn plugin_type(&self) -> &str {
1018 "NDFileNexus"
1019 }
1020
1021 fn does_array_callbacks(&self) -> bool {
1024 false
1025 }
1026
1027 fn register_params(
1028 &mut self,
1029 base: &mut asyn_rs::port::PortDriverBase,
1030 ) -> asyn_rs::error::AsynResult<()> {
1031 self.ctrl.lock().register_params(base)?;
1032 use asyn_rs::param::ParamType;
1033 let path_idx = base.create_param("NEXUS_TEMPLATE_PATH", ParamType::Octet)?;
1034 let file_idx = base.create_param("NEXUS_TEMPLATE_FILE", ParamType::Octet)?;
1035 let valid_idx = base.create_param("NEXUS_TEMPLATE_VALID", ParamType::Int32)?;
1036 base.create_param("TEMPLATE_FILE_PATH", ParamType::Octet)?;
1037 base.create_param("TEMPLATE_FILE_NAME", ParamType::Octet)?;
1038 base.create_param("TEMPLATE_FILE_VALID", ParamType::Int32)?;
1039 base.set_int32_param(valid_idx, 0, 0)?;
1041 self.template_path_idx = Some(path_idx);
1042 self.template_file_idx = Some(file_idx);
1043 self.template_valid_idx = Some(valid_idx);
1044 Ok(())
1045 }
1046
1047 fn on_param_change(&self, reason: usize, params: &PluginParamSnapshot) -> ParamChangeResult {
1048 use ad_core_rs::plugin::runtime::ParamChangeValue;
1049
1050 if Some(reason) == self.template_path_idx {
1053 if let ParamChangeValue::Octet(s) = ¶ms.value {
1054 self.template.lock().path = s.clone();
1055 }
1056 let valid = self.reload_template();
1057 return self.template_valid_result(valid);
1058 }
1059 if Some(reason) == self.template_file_idx {
1060 if let ParamChangeValue::Octet(s) = ¶ms.value {
1061 self.template.lock().file = s.clone();
1062 }
1063 let valid = self.reload_template();
1064 return self.template_valid_result(valid);
1065 }
1066 self.ctrl.lock().on_param_change(reason, params)
1067 }
1068}
1069
1070impl NexusFileProcessor {
1071 fn template_valid_result(&self, valid: i32) -> ParamChangeResult {
1073 use ad_core_rs::plugin::runtime::ParamUpdate;
1074 match self.template_valid_idx {
1075 Some(idx) => ParamChangeResult::updates(vec![ParamUpdate::Int32 {
1076 reason: idx,
1077 addr: 0,
1078 value: valid,
1079 }]),
1080 None => ParamChangeResult::empty(),
1081 }
1082 }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088
1089 fn temp_path(prefix: &str) -> PathBuf {
1090 use std::sync::atomic::{AtomicU32, Ordering};
1091 static COUNTER: AtomicU32 = AtomicU32::new(0);
1092 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1093 std::env::temp_dir().join(format!(
1094 "adcore_test_{}_{}_{}.nxs",
1095 std::process::id(),
1096 prefix,
1097 n
1098 ))
1099 }
1100
1101 #[test]
1102 fn test_nexus_write_read() {
1103 let path = temp_path("nexus_basic");
1104 let mut writer = NexusWriter::new();
1105
1106 let mut arr = NDArray::new(
1107 vec![NDDimension::new(4), NDDimension::new(4)],
1108 NDDataType::UInt8,
1109 );
1110 if let NDDataBuffer::U8(ref mut v) = arr.data {
1111 for i in 0..16 {
1112 v[i] = i as u8;
1113 }
1114 }
1115
1116 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1117 writer.write_file(&arr).unwrap();
1118 writer.close_file().unwrap();
1119
1120 let h5file = H5File::open(&path).unwrap();
1122 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
1123 let data: Vec<u8> = ds.read_raw().unwrap();
1124 assert_eq!(data.len(), 16);
1125 assert_eq!(data[0], 0);
1126 assert_eq!(data[15], 15);
1127
1128 std::fs::remove_file(&path).ok();
1129 }
1130
1131 #[test]
1132 fn test_nexus_multiple_frames() {
1133 let path = temp_path("nexus_multi");
1134 let mut writer = NexusWriter::new();
1135
1136 let mut arr1 = NDArray::new(
1137 vec![NDDimension::new(4), NDDimension::new(4)],
1138 NDDataType::UInt8,
1139 );
1140 if let NDDataBuffer::U8(ref mut v) = arr1.data {
1141 for i in 0..16 {
1142 v[i] = i as u8;
1143 }
1144 }
1145
1146 let mut arr2 = NDArray::new(
1147 vec![NDDimension::new(4), NDDimension::new(4)],
1148 NDDataType::UInt8,
1149 );
1150 if let NDDataBuffer::U8(ref mut v) = arr2.data {
1151 for i in 0..16 {
1152 v[i] = (i + 100) as u8;
1153 }
1154 }
1155
1156 writer.open_file(&path, NDFileMode::Stream, &arr1).unwrap();
1157 writer.write_file(&arr1).unwrap();
1158 writer.write_file(&arr2).unwrap();
1159 writer.close_file().unwrap();
1160
1161 assert_eq!(writer.frame_count(), 2);
1162
1163 let h5file = H5File::open(&path).unwrap();
1165 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
1166 let shape = ds.shape();
1167 assert_eq!(shape, vec![2, 4, 4]);
1168
1169 let data: Vec<u8> = ds.read_raw().unwrap();
1170 assert_eq!(data.len(), 32);
1171 assert_eq!(data[0], 0);
1173 assert_eq!(data[15], 15);
1174 assert_eq!(data[16], 100);
1176 assert_eq!(data[31], 115);
1177
1178 std::fs::remove_file(&path).ok();
1179 }
1180
1181 #[test]
1182 fn test_per_frame_metadata_are_datasets_not_attributes() {
1183 let path = temp_path("nexus_meta");
1184 let mut writer = NexusWriter::new();
1185
1186 let mut a1 = NDArray::new(
1187 vec![NDDimension::new(2), NDDimension::new(2)],
1188 NDDataType::UInt8,
1189 );
1190 a1.unique_id = 10;
1191 a1.time_stamp = 1.5;
1192 let mut a2 = NDArray::new(
1193 vec![NDDimension::new(2), NDDimension::new(2)],
1194 NDDataType::UInt8,
1195 );
1196 a2.unique_id = 11;
1197 a2.time_stamp = 2.5;
1198
1199 writer.open_file(&path, NDFileMode::Stream, &a1).unwrap();
1200 writer.write_file(&a1).unwrap();
1201 writer.write_file(&a2).unwrap();
1202 writer.close_file().unwrap();
1203
1204 let h5file = H5File::open(&path).unwrap();
1205 let uid = h5file
1208 .dataset("entry/instrument/detector/uniqueId")
1209 .unwrap();
1210 assert_eq!(uid.shape(), vec![2]);
1211 let uid_data: Vec<i32> = uid.read_raw().unwrap();
1212 assert_eq!(uid_data, vec![10, 11]);
1213
1214 let ts = h5file
1215 .dataset("entry/instrument/detector/timeStamp")
1216 .unwrap();
1217 assert_eq!(ts.shape(), vec![2]);
1218 let ts_data: Vec<f64> = ts.read_raw().unwrap();
1219 assert_eq!(ts_data, vec![1.5, 2.5]);
1220
1221 std::fs::remove_file(&path).ok();
1222 }
1223
1224 #[test]
1225 fn test_xml_template_parser() {
1226 let xml = r#"<?xml version="1.0"?>
1227 <NXroot>
1228 <NXentry name="entry">
1229 <NXdata name="data">
1230 <data type="pArray"/>
1231 </NXdata>
1232 <title>My Experiment</title>
1233 </NXentry>
1234 </NXroot>"#;
1235 let root = parse_nexus_template(xml).unwrap();
1236 assert_eq!(root.name, "NXroot");
1237 assert_eq!(root.children.len(), 1);
1238 let entry = &root.children[0];
1239 assert_eq!(entry.name, "NXentry");
1240 assert_eq!(entry.attr("name"), Some("entry"));
1241 assert_eq!(entry.children.len(), 2);
1242 assert_eq!(entry.children[1].name, "title");
1243 assert_eq!(entry.children[1].text, "My Experiment");
1244 let data = &entry.children[0].children[0];
1245 assert_eq!(data.attr("type"), Some("pArray"));
1246 }
1247
1248 #[test]
1249 fn test_template_drives_file_structure() {
1250 let xml = r#"<NXroot>
1252 <NXentry name="scan">
1253 <NXdata name="measurement">
1254 <frames type="pArray"/>
1255 <title>const-title</title>
1256 </NXdata>
1257 </NXentry>
1258 </NXroot>"#;
1259 let mut writer = NexusWriter::new();
1260 assert!(writer.load_template(xml));
1261 assert!(writer.has_template());
1262
1263 let path = temp_path("nexus_tmpl");
1264 let mut arr = NDArray::new(
1265 vec![NDDimension::new(3), NDDimension::new(2)],
1266 NDDataType::UInt8,
1267 );
1268 if let NDDataBuffer::U8(ref mut v) = arr.data {
1269 for (i, x) in v.iter_mut().enumerate() {
1270 *x = i as u8;
1271 }
1272 }
1273 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1274 writer.write_file(&arr).unwrap();
1275 writer.close_file().unwrap();
1276
1277 let h5file = H5File::open(&path).unwrap();
1278 let ds = h5file.dataset("scan/measurement/frames").unwrap();
1280 assert_eq!(ds.shape(), vec![1, 2, 3]);
1281 let title = h5file.dataset("scan/measurement/title").unwrap();
1283 let title_bytes: Vec<u8> = title.read_raw().unwrap();
1284 assert_eq!(
1285 String::from_utf8_lossy(&title_bytes).trim_end_matches('\0'),
1286 "const-title"
1287 );
1288
1289 std::fs::remove_file(&path).ok();
1290 }
1291
1292 #[test]
1293 fn test_load_template_invalid_xml_returns_false() {
1294 let mut writer = NexusWriter::new();
1295 assert!(!writer.load_template("<NXroot><unclosed>"));
1296 assert!(!writer.has_template());
1297 }
1298
1299 #[test]
1300 fn test_template_param_change_sets_valid_flag() {
1301 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1302 use asyn_rs::port::{PortDriverBase, PortFlags};
1303
1304 let dir = std::env::temp_dir();
1305 let tmpl_path = dir.join(format!(
1306 "adcore_nexus_template_test_{}.xml",
1307 std::process::id()
1308 ));
1309 std::fs::write(
1310 &tmpl_path,
1311 r#"<NXroot><NXentry name="e"><NXdata name="d"><x type="pArray"/></NXdata></NXentry></NXroot>"#,
1312 )
1313 .unwrap();
1314
1315 let mut base = PortDriverBase::new("nexus_tmpl_test", 1, PortFlags::default());
1316 let mut proc = NexusFileProcessor::new();
1317 proc.register_params(&mut base).unwrap();
1318
1319 let file_idx = proc.template_file_idx.unwrap();
1320 let valid_idx = proc.template_valid_idx.unwrap();
1321
1322 let result = proc.on_param_change(
1323 file_idx,
1324 &PluginParamSnapshot {
1325 enable_callbacks: true,
1326 reason: file_idx,
1327 addr: 0,
1328 value: ParamChangeValue::Octet(tmpl_path.to_string_lossy().into_owned()),
1329 },
1330 );
1331 assert!(result.param_updates.iter().any(|u| matches!(
1332 u,
1333 ParamUpdate::Int32 { reason, value: 1, .. } if *reason == valid_idx
1334 )));
1335 assert!(proc.ctrl.lock().writer.has_template());
1336
1337 std::fs::remove_file(&tmpl_path).ok();
1338 }
1339
1340 #[test]
1341 fn test_nxdata_group_contains_image_data() {
1342 let path = temp_path("nexus_nxdata");
1345 let mut writer = NexusWriter::new();
1346
1347 let mk = |base: u8| {
1348 let mut arr = NDArray::new(
1349 vec![NDDimension::new(3), NDDimension::new(2)],
1350 NDDataType::UInt8,
1351 );
1352 if let NDDataBuffer::U8(ref mut v) = arr.data {
1353 for (i, x) in v.iter_mut().enumerate() {
1354 *x = base + i as u8;
1355 }
1356 }
1357 arr
1358 };
1359
1360 let a0 = mk(0);
1361 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
1362 writer.write_file(&a0).unwrap();
1363 writer.write_file(&mk(100)).unwrap();
1364 writer.close_file().unwrap();
1365
1366 let h5 = H5File::open(&path).unwrap();
1367 let nx = h5
1369 .dataset("entry/data/data")
1370 .expect("NXdata group must contain the `data` dataset");
1371 assert_eq!(nx.shape(), vec![2, 2, 3]);
1372 let nx_vals: Vec<u8> = nx.read_raw().unwrap();
1373 assert_eq!(nx_vals.len(), 2 * 6);
1374 assert_eq!(nx_vals[0], 0);
1375 assert_eq!(nx_vals[6], 100);
1376 let det: Vec<u8> = h5
1378 .dataset("entry/instrument/detector/data")
1379 .unwrap()
1380 .read_raw()
1381 .unwrap();
1382 assert_eq!(nx_vals, det);
1383
1384 std::fs::remove_file(&path).ok();
1385 }
1386
1387 #[test]
1388 fn test_nx_class_is_true_group_attribute() {
1389 let path = temp_path("nexus_nxclass");
1392 let mut writer = NexusWriter::new();
1393 let mut arr = NDArray::new(
1394 vec![NDDimension::new(3), NDDimension::new(2)],
1395 NDDataType::UInt8,
1396 );
1397 if let NDDataBuffer::U8(ref mut v) = arr.data {
1398 v.iter_mut().enumerate().for_each(|(i, x)| *x = i as u8);
1399 }
1400 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1401 writer.write_file(&arr).unwrap();
1402 writer.close_file().unwrap();
1403
1404 let h5 = H5File::open(&path).unwrap();
1405 for (group, class) in [
1407 ("entry", "NXentry"),
1408 ("entry/instrument", "NXinstrument"),
1409 ("entry/instrument/detector", "NXdetector"),
1410 ("entry/data", "NXdata"),
1411 ] {
1412 let g = h5.root_group().group(group).expect("group exists");
1413 let got = g
1414 .attr_string("NX_class")
1415 .unwrap_or_else(|_| panic!("{} must have NX_class group attribute", group));
1416 assert_eq!(got, class, "{} NX_class", group);
1417 }
1418 assert!(h5.dataset("entry/NX_class").is_err());
1420
1421 std::fs::remove_file(&path).ok();
1422 }
1423
1424 #[test]
1425 fn test_read_file_roundtrips_all_data_types() {
1426 use ad_core_rs::ndarray::NDDataType::*;
1429 for dt in [
1430 Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64,
1431 ] {
1432 let path = temp_path(&format!("nexus_type_{:?}", dt));
1433 let mut writer = NexusWriter::new();
1434 let mut arr = NDArray::new(vec![NDDimension::new(2), NDDimension::new(2)], dt);
1435 match arr.data {
1437 NDDataBuffer::I8(ref mut v) => v.copy_from_slice(&[-1, 2, -3, 4]),
1438 NDDataBuffer::U8(ref mut v) => v.copy_from_slice(&[1, 2, 3, 4]),
1439 NDDataBuffer::I16(ref mut v) => v.copy_from_slice(&[-1, 2, -3, 4]),
1440 NDDataBuffer::U16(ref mut v) => v.copy_from_slice(&[1, 2, 3, 4]),
1441 NDDataBuffer::I32(ref mut v) => v.copy_from_slice(&[-1, 2, -3, 4]),
1442 NDDataBuffer::U32(ref mut v) => v.copy_from_slice(&[1, 2, 3, 4]),
1443 NDDataBuffer::I64(ref mut v) => v.copy_from_slice(&[-1, 2, -3, 4]),
1444 NDDataBuffer::U64(ref mut v) => v.copy_from_slice(&[1, 2, 3, 4]),
1445 NDDataBuffer::F32(ref mut v) => v.copy_from_slice(&[-1.5, 2.5, -3.5, 4.5]),
1446 NDDataBuffer::F64(ref mut v) => v.copy_from_slice(&[-1.5, 2.5, -3.5, 4.5]),
1447 }
1448
1449 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1450 writer.write_file(&arr).unwrap();
1451 writer.close_file().unwrap();
1452
1453 let mut reader = NexusWriter::new();
1454 reader.current_path = Some(path.clone());
1455 let read = reader
1456 .read_file()
1457 .unwrap_or_else(|e| panic!("{:?} read failed: {}", dt, e));
1458 assert_eq!(read.data.data_type(), dt, "{:?}: type must round-trip", dt);
1459 assert_eq!(read.data.len(), 4, "{:?}: element count", dt);
1461 for i in 0..4 {
1462 assert_eq!(
1463 arr.data.get_as_f64(i),
1464 read.data.get_as_f64(i),
1465 "{:?}: element {} must round-trip",
1466 dt,
1467 i
1468 );
1469 }
1470
1471 std::fs::remove_file(&path).ok();
1472 }
1473 }
1474}