Skip to main content

ad_plugins_rs/
file_nexus.rs

1//! NeXus file writer plugin.
2//!
3//! Writes NDArray data in NeXus/HDF5 format using the rust-hdf5 library.
4//! Follows the simplified NXdata convention:
5//!
6//! ```text
7//! /entry (NX_class=NXentry)
8//!   /instrument (NX_class=NXinstrument)
9//!     /detector (NX_class=NXdetector)
10//!       /data → dataset [frames × Y × X]
11//!   /data (NX_class=NXdata)
12//!     /data → same dataset
13//! ```
14
15use 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::H5File;
27
28/// NeXus file writer using HDF5 with NeXus group structure.
29pub struct NexusWriter {
30    current_path: Option<PathBuf>,
31    file: Option<H5File>,
32    frame_count: usize,
33}
34
35impl NexusWriter {
36    pub fn new() -> Self {
37        Self {
38            current_path: None,
39            file: None,
40            frame_count: 0,
41        }
42    }
43
44    pub fn frame_count(&self) -> usize {
45        self.frame_count
46    }
47}
48
49impl Default for NexusWriter {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl NDFileWriter for NexusWriter {
56    fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
57        self.current_path = Some(path.to_path_buf());
58        self.frame_count = 0;
59
60        let h5file = H5File::create(path)
61            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus create error: {}", e)))?;
62
63        // Create NeXus group hierarchy
64        let entry = h5file
65            .create_group("entry")
66            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
67        let instrument = entry
68            .create_group("instrument")
69            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
70        let _detector = instrument
71            .create_group("detector")
72            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
73        let _data_group = entry
74            .create_group("data")
75            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus group error: {}", e)))?;
76
77        self.file = Some(h5file);
78        Ok(())
79    }
80
81    fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
82        let h5file = self
83            .file
84            .as_ref()
85            .ok_or_else(|| ADError::UnsupportedConversion("no NeXus file open".into()))?;
86
87        let shape = array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>();
88
89        // Write data into /entry/instrument/detector/data_N
90        // (each frame as a separate dataset, like the base HDF5 writer)
91        let dataset_name = if self.frame_count == 0 {
92            "data".to_string()
93        } else {
94            format!("data_{}", self.frame_count)
95        };
96
97        // Create dataset inside detector group
98        let detector_group = h5file
99            .root_group()
100            .group("entry")
101            .map_err(|e| ADError::UnsupportedConversion(e.to_string()))?
102            .group("instrument")
103            .map_err(|e| ADError::UnsupportedConversion(e.to_string()))?
104            .group("detector")
105            .map_err(|e| ADError::UnsupportedConversion(e.to_string()))?;
106
107        macro_rules! write_typed {
108            ($t:ty, $v:expr) => {{
109                let ds = detector_group
110                    .new_dataset::<$t>()
111                    .shape(&shape[..])
112                    .create(&dataset_name)
113                    .map_err(|e| {
114                        ADError::UnsupportedConversion(format!("NeXus dataset error: {}", e))
115                    })?;
116                ds.write_raw($v).map_err(|e| {
117                    ADError::UnsupportedConversion(format!("NeXus write error: {}", e))
118                })?;
119                // Write NDArray attributes
120                for attr in array.attributes.iter() {
121                    let val_str = attr.value.as_string();
122                    let _ = ds
123                        .new_attr::<rust_hdf5::types::VarLenUnicode>()
124                        .shape(())
125                        .create(attr.name.as_str())
126                        .and_then(|a| {
127                            let s: rust_hdf5::types::VarLenUnicode =
128                                val_str.parse().unwrap_or_default();
129                            a.write_scalar(&s)
130                        });
131                }
132            }};
133        }
134
135        match &array.data {
136            NDDataBuffer::U8(v) => write_typed!(u8, v),
137            NDDataBuffer::U16(v) => write_typed!(u16, v),
138            NDDataBuffer::I16(v) => write_typed!(i16, v),
139            NDDataBuffer::I32(v) => write_typed!(i32, v),
140            NDDataBuffer::U32(v) => write_typed!(u32, v),
141            NDDataBuffer::F32(v) => write_typed!(f32, v),
142            NDDataBuffer::F64(v) => write_typed!(f64, v),
143            _ => {
144                let raw = array.data.as_u8_slice();
145                let ds = detector_group
146                    .new_dataset::<u8>()
147                    .shape([raw.len()])
148                    .create(&dataset_name)
149                    .map_err(|e| {
150                        ADError::UnsupportedConversion(format!("NeXus dataset error: {}", e))
151                    })?;
152                ds.write_raw(raw).map_err(|e| {
153                    ADError::UnsupportedConversion(format!("NeXus write error: {}", e))
154                })?;
155            }
156        }
157
158        self.frame_count += 1;
159        Ok(())
160    }
161
162    fn read_file(&mut self) -> ADResult<NDArray> {
163        let path = self
164            .current_path
165            .as_ref()
166            .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
167
168        let h5file = H5File::open(path)
169            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus open error: {}", e)))?;
170
171        // Try reading from /entry/instrument/detector/data
172        let ds = h5file
173            .dataset("entry/instrument/detector/data")
174            .map_err(|e| ADError::UnsupportedConversion(format!("NeXus dataset error: {}", e)))?;
175
176        let shape = ds.shape();
177        let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
178
179        if let Ok(data) = ds.read_raw::<u8>() {
180            let mut arr = NDArray::new(dims, NDDataType::UInt8);
181            arr.data = NDDataBuffer::U8(data);
182            return Ok(arr);
183        }
184        if let Ok(data) = ds.read_raw::<u16>() {
185            let mut arr = NDArray::new(dims, NDDataType::UInt16);
186            arr.data = NDDataBuffer::U16(data);
187            return Ok(arr);
188        }
189        if let Ok(data) = ds.read_raw::<f64>() {
190            let mut arr = NDArray::new(dims, NDDataType::Float64);
191            arr.data = NDDataBuffer::F64(data);
192            return Ok(arr);
193        }
194
195        Err(ADError::UnsupportedConversion(
196            "unsupported data type in NeXus file".into(),
197        ))
198    }
199
200    fn close_file(&mut self) -> ADResult<()> {
201        self.file = None;
202        self.current_path = None;
203        Ok(())
204    }
205
206    fn supports_multiple_arrays(&self) -> bool {
207        true
208    }
209}
210
211// ============================================================
212// Processor
213// ============================================================
214
215pub struct NexusFileProcessor {
216    ctrl: FilePluginController<NexusWriter>,
217}
218
219impl NexusFileProcessor {
220    pub fn new() -> Self {
221        Self {
222            ctrl: FilePluginController::new(NexusWriter::new()),
223        }
224    }
225}
226
227impl Default for NexusFileProcessor {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233impl NDPluginProcess for NexusFileProcessor {
234    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
235        self.ctrl.process_array(array)
236    }
237
238    fn plugin_type(&self) -> &str {
239        "NDFileNexus"
240    }
241
242    fn register_params(
243        &mut self,
244        base: &mut asyn_rs::port::PortDriverBase,
245    ) -> asyn_rs::error::AsynResult<()> {
246        self.ctrl.register_params(base)?;
247        use asyn_rs::param::ParamType;
248        base.create_param("NEXUS_TEMPLATE_PATH", ParamType::Octet)?;
249        base.create_param("NEXUS_TEMPLATE_FILE", ParamType::Octet)?;
250        base.create_param("NEXUS_TEMPLATE_VALID", ParamType::Int32)?;
251        Ok(())
252    }
253
254    fn on_param_change(
255        &mut self,
256        reason: usize,
257        params: &PluginParamSnapshot,
258    ) -> ParamChangeResult {
259        self.ctrl.on_param_change(reason, params)
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn temp_path(prefix: &str) -> PathBuf {
268        use std::sync::atomic::{AtomicU32, Ordering};
269        static COUNTER: AtomicU32 = AtomicU32::new(0);
270        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
271        std::env::temp_dir().join(format!("adcore_test_{}_{}.nxs", prefix, n))
272    }
273
274    #[test]
275    fn test_nexus_write_read() {
276        let path = temp_path("nexus_basic");
277        let mut writer = NexusWriter::new();
278
279        let mut arr = NDArray::new(
280            vec![NDDimension::new(4), NDDimension::new(4)],
281            NDDataType::UInt8,
282        );
283        if let NDDataBuffer::U8(ref mut v) = arr.data {
284            for i in 0..16 {
285                v[i] = i as u8;
286            }
287        }
288
289        writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
290        writer.write_file(&arr).unwrap();
291        writer.close_file().unwrap();
292
293        // Verify NeXus structure
294        let h5file = H5File::open(&path).unwrap();
295        let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
296        let data: Vec<u8> = ds.read_raw().unwrap();
297        assert_eq!(data.len(), 16);
298        assert_eq!(data[0], 0);
299        assert_eq!(data[15], 15);
300
301        std::fs::remove_file(&path).ok();
302    }
303
304    #[test]
305    fn test_nexus_multiple_frames() {
306        let path = temp_path("nexus_multi");
307        let mut writer = NexusWriter::new();
308
309        let arr = NDArray::new(
310            vec![NDDimension::new(4), NDDimension::new(4)],
311            NDDataType::UInt8,
312        );
313
314        writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
315        writer.write_file(&arr).unwrap();
316        writer.write_file(&arr).unwrap();
317        writer.close_file().unwrap();
318
319        assert_eq!(writer.frame_count(), 2);
320
321        let data = std::fs::read(&path).unwrap();
322        assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
323
324        std::fs::remove_file(&path).ok();
325    }
326}