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