1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
11};
12
13use netcdf3::{DataSet, FileReader, FileWriter, Version};
14
15const VAR_NAME: &str = "array_data";
16const DIM_UNLIMITED: &str = "numArrays";
17
18struct DimMeta {
20 size: usize,
21 offset: usize,
22 binning: usize,
23 reverse: bool,
24}
25
26struct AttrData {
28 name: String,
29 description: String,
30 source: String,
32 source_type: String,
34 data_type_string: String,
36 value: NDAttrValue,
37}
38
39struct FrameData {
41 dims: Vec<usize>,
42 dim_meta: Vec<DimMeta>,
43 data: NDDataBuffer,
44 data_type: NDDataType,
45 attrs: Vec<AttrData>,
46 unique_id: i32,
47 time_stamp: f64,
48 epics_ts_sec: i32,
49 epics_ts_nsec: i32,
50}
51
52fn attr_source_type_string(src: &NDAttrSource) -> &'static str {
55 match src {
56 NDAttrSource::Driver => "NDAttrSourceDriver",
57 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
58 NDAttrSource::Param { .. } => "NDAttrSourceParam",
59 NDAttrSource::Function(_) => "NDAttrSourceFunct",
60 NDAttrSource::Constant(_) => "NDAttrSourceConst",
61 NDAttrSource::Undefined => "Undefined",
62 }
63}
64
65fn attr_data_type_string(value: &NDAttrValue) -> &'static str {
67 match value {
68 NDAttrValue::Int8(_) => "Int8",
69 NDAttrValue::UInt8(_) => "UInt8",
70 NDAttrValue::Int16(_) => "Int16",
71 NDAttrValue::UInt16(_) => "UInt16",
72 NDAttrValue::Int32(_) => "Int32",
73 NDAttrValue::UInt32(_) => "UInt32",
74 NDAttrValue::Int64(_) => "Int64",
75 NDAttrValue::UInt64(_) => "UInt64",
76 NDAttrValue::Float32(_) => "Float32",
77 NDAttrValue::Float64(_) => "Float64",
78 NDAttrValue::String(_) => "String",
79 NDAttrValue::Undefined => "Undefined",
80 }
81}
82
83pub struct NetcdfWriter {
91 current_path: Option<PathBuf>,
92 frames: Vec<FrameData>,
93}
94
95impl NetcdfWriter {
96 pub fn new() -> Self {
97 Self {
98 current_path: None,
99 frames: Vec::new(),
100 }
101 }
102}
103
104fn nc_data_type(dt: NDDataType) -> ADResult<netcdf3::DataType> {
107 match dt {
108 NDDataType::Int8 => Ok(netcdf3::DataType::I8),
109 NDDataType::UInt8 => Ok(netcdf3::DataType::U8),
110 NDDataType::Int16 | NDDataType::UInt16 => Ok(netcdf3::DataType::I16),
111 NDDataType::Int32 | NDDataType::UInt32 => Ok(netcdf3::DataType::I32),
112 NDDataType::Float32 => Ok(netcdf3::DataType::F32),
113 NDDataType::Float64 => Ok(netcdf3::DataType::F64),
114 NDDataType::Int64 | NDDataType::UInt64 => Ok(netcdf3::DataType::F64),
115 }
116}
117
118fn write_var_data(writer: &mut FileWriter, data: &NDDataBuffer) -> ADResult<()> {
120 let err = |e: netcdf3::error::WriteError| {
121 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
122 };
123 match data {
124 NDDataBuffer::I8(v) => writer.write_var_i8(VAR_NAME, v).map_err(err),
125 NDDataBuffer::U8(v) => writer.write_var_u8(VAR_NAME, v).map_err(err),
126 NDDataBuffer::I16(v) => writer.write_var_i16(VAR_NAME, v).map_err(err),
127 NDDataBuffer::U16(v) => {
128 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
129 writer.write_var_i16(VAR_NAME, &reinterp).map_err(err)
130 }
131 NDDataBuffer::I32(v) => writer.write_var_i32(VAR_NAME, v).map_err(err),
132 NDDataBuffer::U32(v) => {
133 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
134 writer.write_var_i32(VAR_NAME, &reinterp).map_err(err)
135 }
136 NDDataBuffer::F32(v) => writer.write_var_f32(VAR_NAME, v).map_err(err),
137 NDDataBuffer::F64(v) => writer.write_var_f64(VAR_NAME, v).map_err(err),
138 NDDataBuffer::I64(v) => {
139 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
140 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
141 }
142 NDDataBuffer::U64(v) => {
143 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
144 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
145 }
146 }
147}
148
149fn write_record_data(
151 writer: &mut FileWriter,
152 record_index: usize,
153 data: &NDDataBuffer,
154) -> ADResult<()> {
155 let err = |e: netcdf3::error::WriteError| {
156 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
157 };
158 match data {
159 NDDataBuffer::I8(v) => writer
160 .write_record_i8(VAR_NAME, record_index, v)
161 .map_err(err),
162 NDDataBuffer::U8(v) => writer
163 .write_record_u8(VAR_NAME, record_index, v)
164 .map_err(err),
165 NDDataBuffer::I16(v) => writer
166 .write_record_i16(VAR_NAME, record_index, v)
167 .map_err(err),
168 NDDataBuffer::U16(v) => {
169 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
170 writer
171 .write_record_i16(VAR_NAME, record_index, &reinterp)
172 .map_err(err)
173 }
174 NDDataBuffer::I32(v) => writer
175 .write_record_i32(VAR_NAME, record_index, v)
176 .map_err(err),
177 NDDataBuffer::U32(v) => {
178 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
179 writer
180 .write_record_i32(VAR_NAME, record_index, &reinterp)
181 .map_err(err)
182 }
183 NDDataBuffer::F32(v) => writer
184 .write_record_f32(VAR_NAME, record_index, v)
185 .map_err(err),
186 NDDataBuffer::F64(v) => writer
187 .write_record_f64(VAR_NAME, record_index, v)
188 .map_err(err),
189 NDDataBuffer::I64(v) => {
190 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
191 writer
192 .write_record_f64(VAR_NAME, record_index, &reinterp)
193 .map_err(err)
194 }
195 NDDataBuffer::U64(v) => {
196 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
197 writer
198 .write_record_f64(VAR_NAME, record_index, &reinterp)
199 .map_err(err)
200 }
201 }
202}
203
204const ATTR_STRING_DIM: &str = "attrStringSize";
205const ATTR_STRING_SIZE: usize = 256;
206
207fn attr_nc_type(value: &NDAttrValue) -> netcdf3::DataType {
209 match value {
210 NDAttrValue::Int8(_) | NDAttrValue::UInt8(_) | NDAttrValue::Undefined => {
211 netcdf3::DataType::I8
212 }
213 NDAttrValue::Int16(_) | NDAttrValue::UInt16(_) => netcdf3::DataType::I16,
214 NDAttrValue::Int32(_) | NDAttrValue::UInt32(_) => netcdf3::DataType::I32,
215 NDAttrValue::Float32(_) => netcdf3::DataType::F32,
216 NDAttrValue::Float64(_) | NDAttrValue::Int64(_) | NDAttrValue::UInt64(_) => {
217 netcdf3::DataType::F64
218 }
219 NDAttrValue::String(_) => netcdf3::DataType::I8,
220 }
221}
222
223fn write_attr_value(
226 writer: &mut FileWriter,
227 var_name: &str,
228 record_index: usize,
229 multi: bool,
230 value: &NDAttrValue,
231) -> ADResult<()> {
232 let werr = |e: netcdf3::error::WriteError| {
233 ADError::UnsupportedConversion(format!("NetCDF attr write error: {:?}", e))
234 };
235 if let NDAttrValue::String(s) = value {
237 let mut bytes: Vec<i8> = s.bytes().take(ATTR_STRING_SIZE).map(|b| b as i8).collect();
238 bytes.resize(ATTR_STRING_SIZE, 0);
239 return if multi {
240 writer
241 .write_record_i8(var_name, record_index, &bytes)
242 .map_err(werr)
243 } else {
244 writer.write_var_i8(var_name, &bytes).map_err(werr)
245 };
246 }
247 match attr_nc_type(value) {
248 netcdf3::DataType::I8 => {
249 let v = value.as_i64().unwrap_or(0) as i8;
250 if multi {
251 writer
252 .write_record_i8(var_name, record_index, &[v])
253 .map_err(werr)
254 } else {
255 writer.write_var_i8(var_name, &[v]).map_err(werr)
256 }
257 }
258 netcdf3::DataType::I16 => {
259 let v = value.as_i64().unwrap_or(0) as i16;
260 if multi {
261 writer
262 .write_record_i16(var_name, record_index, &[v])
263 .map_err(werr)
264 } else {
265 writer.write_var_i16(var_name, &[v]).map_err(werr)
266 }
267 }
268 netcdf3::DataType::I32 => {
269 let v = value.as_i64().unwrap_or(0) as i32;
270 if multi {
271 writer
272 .write_record_i32(var_name, record_index, &[v])
273 .map_err(werr)
274 } else {
275 writer.write_var_i32(var_name, &[v]).map_err(werr)
276 }
277 }
278 netcdf3::DataType::F32 => {
279 let v = value.as_f64().unwrap_or(0.0) as f32;
280 if multi {
281 writer
282 .write_record_f32(var_name, record_index, &[v])
283 .map_err(werr)
284 } else {
285 writer.write_var_f32(var_name, &[v]).map_err(werr)
286 }
287 }
288 netcdf3::DataType::F64 => {
289 let v = value.as_f64().unwrap_or(0.0);
290 if multi {
291 writer
292 .write_record_f64(var_name, record_index, &[v])
293 .map_err(werr)
294 } else {
295 writer.write_var_f64(var_name, &[v]).map_err(werr)
296 }
297 }
298 netcdf3::DataType::U8 => unreachable!("attr_nc_type never returns U8"),
299 }
300}
301
302impl NDFileWriter for NetcdfWriter {
303 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
304 self.current_path = Some(path.to_path_buf());
305 self.frames.clear();
306 Ok(())
307 }
308
309 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
310 nc_data_type(array.data.data_type())?;
312
313 let dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
314 let dim_meta: Vec<DimMeta> = array
315 .dims
316 .iter()
317 .map(|d| DimMeta {
318 size: d.size,
319 offset: d.offset,
320 binning: d.binning,
321 reverse: d.reverse,
322 })
323 .collect();
324 let attrs: Vec<AttrData> = array
325 .attributes
326 .iter()
327 .map(|a| AttrData {
328 name: a.name.clone(),
329 description: a.description.clone(),
330 source: a.source.source_string().to_string(),
333 source_type: attr_source_type_string(&a.source).to_string(),
334 data_type_string: attr_data_type_string(&a.value).to_string(),
335 value: a.value.clone(),
336 })
337 .collect();
338
339 self.frames.push(FrameData {
340 dims,
341 dim_meta,
342 data: array.data.clone(),
343 data_type: array.data.data_type(),
344 attrs,
345 unique_id: array.unique_id,
346 time_stamp: array.time_stamp,
347 epics_ts_sec: array.timestamp.sec as i32,
348 epics_ts_nsec: array.timestamp.nsec as i32,
349 });
350 Ok(())
351 }
352
353 fn close_file(&mut self) -> ADResult<()> {
354 let path = match self.current_path.take() {
355 Some(p) => p,
356 None => return Ok(()),
357 };
358
359 if self.frames.is_empty() {
360 return Ok(());
361 }
362
363 let map_def = |e: netcdf3::error::InvalidDataSet| {
364 ADError::UnsupportedConversion(format!("NetCDF definition error: {:?}", e))
365 };
366 let map_write = |e: netcdf3::error::WriteError| {
367 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
368 };
369
370 let first = &self.frames[0];
371 let nc_dt = nc_data_type(first.data_type)?;
372 let multi = self.frames.len() > 1;
373
374 let mut ds = DataSet::new();
376
377 if multi {
383 ds.set_unlimited_dim(DIM_UNLIMITED, self.frames.len())
384 .map_err(map_def)?;
385 } else {
386 ds.add_fixed_dim(DIM_UNLIMITED, 1).map_err(map_def)?;
387 }
388
389 let ndims = first.dims.len();
391 let mut dim_names: Vec<String> = Vec::new();
392 for i in 0..ndims {
393 let dim_idx = ndims - 1 - i;
394 let name = format!("dim{}", i);
395 ds.add_fixed_dim(&name, first.dims[dim_idx])
396 .map_err(map_def)?;
397 dim_names.push(name);
398 }
399
400 let has_string_attr = self.frames.iter().any(|f| {
402 f.attrs
403 .iter()
404 .any(|a| matches!(a.value, NDAttrValue::String(_)))
405 });
406 if has_string_attr {
407 ds.add_fixed_dim(ATTR_STRING_DIM, ATTR_STRING_SIZE)
408 .map_err(map_def)?;
409 }
410
411 let var_dims: Vec<String> = {
413 let mut v = vec![DIM_UNLIMITED.to_string()];
414 v.extend(dim_names.iter().cloned());
415 v
416 };
417 let var_dim_refs: Vec<&str> = var_dims.iter().map(|s| s.as_str()).collect();
418 ds.add_var(VAR_NAME, &var_dim_refs, nc_dt)
419 .map_err(map_def)?;
420
421 ds.add_var("uniqueId", &[DIM_UNLIMITED], netcdf3::DataType::I32)
423 .map_err(map_def)?;
424 ds.add_var("timeStamp", &[DIM_UNLIMITED], netcdf3::DataType::F64)
425 .map_err(map_def)?;
426 ds.add_var("epicsTSSec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
427 .map_err(map_def)?;
428 ds.add_var("epicsTSNsec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
429 .map_err(map_def)?;
430
431 let mut attr_var_names: Vec<String> = Vec::new();
436 for attr in &first.attrs {
437 let var_name = format!("Attr_{}", attr.name);
438 let nc_type = attr_nc_type(&attr.value);
439 let is_string = matches!(attr.value, NDAttrValue::String(_));
440 if is_string {
441 ds.add_var(
442 &var_name,
443 &[DIM_UNLIMITED, ATTR_STRING_DIM],
444 netcdf3::DataType::I8,
445 )
446 .map_err(map_def)?;
447 } else {
448 ds.add_var(&var_name, &[DIM_UNLIMITED], nc_type)
449 .map_err(map_def)?;
450 }
451 attr_var_names.push(var_name);
452
453 ds.add_global_attr_string(
454 &format!("Attr_{}_DataType", attr.name),
455 &attr.data_type_string,
456 )
457 .map_err(map_def)?;
458 ds.add_global_attr_string(
459 &format!("Attr_{}_Description", attr.name),
460 &attr.description,
461 )
462 .map_err(map_def)?;
463 ds.add_global_attr_string(&format!("Attr_{}_Source", attr.name), &attr.source)
464 .map_err(map_def)?;
465 ds.add_global_attr_string(&format!("Attr_{}_SourceType", attr.name), &attr.source_type)
466 .map_err(map_def)?;
467 }
468
469 ds.add_global_attr_i32("uniqueId", vec![first.unique_id])
471 .map_err(map_def)?;
472 ds.add_global_attr_i32("dataType", vec![first.data_type as i32])
473 .map_err(map_def)?;
474 ds.add_global_attr_i32("numArrays", vec![self.frames.len() as i32])
475 .map_err(map_def)?;
476
477 ds.add_global_attr_i32("numArrayDims", vec![ndims as i32])
479 .map_err(map_def)?;
480 let dim_size: Vec<i32> = first.dim_meta.iter().map(|d| d.size as i32).collect();
481 ds.add_global_attr_i32("dimSize", dim_size)
482 .map_err(map_def)?;
483 let dim_offset: Vec<i32> = first.dim_meta.iter().map(|d| d.offset as i32).collect();
484 ds.add_global_attr_i32("dimOffset", dim_offset)
485 .map_err(map_def)?;
486 let dim_binning: Vec<i32> = first.dim_meta.iter().map(|d| d.binning as i32).collect();
487 ds.add_global_attr_i32("dimBinning", dim_binning)
488 .map_err(map_def)?;
489 let dim_reverse: Vec<i32> = first
490 .dim_meta
491 .iter()
492 .map(|d| if d.reverse { 1 } else { 0 })
493 .collect();
494 ds.add_global_attr_i32("dimReverse", dim_reverse)
495 .map_err(map_def)?;
496
497 let mut writer = FileWriter::open(&path).map_err(map_write)?;
499 writer
500 .set_def(&ds, Version::Classic, 0)
501 .map_err(map_write)?;
502
503 if multi {
504 for (i, frame) in self.frames.iter().enumerate() {
505 write_record_data(&mut writer, i, &frame.data)?;
506 writer
507 .write_record_i32("uniqueId", i, &[frame.unique_id])
508 .map_err(map_write)?;
509 writer
510 .write_record_f64("timeStamp", i, &[frame.time_stamp])
511 .map_err(map_write)?;
512 writer
513 .write_record_i32("epicsTSSec", i, &[frame.epics_ts_sec])
514 .map_err(map_write)?;
515 writer
516 .write_record_i32("epicsTSNsec", i, &[frame.epics_ts_nsec])
517 .map_err(map_write)?;
518 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
521 let value = frame
522 .attrs
523 .iter()
524 .find(|a| a.name == attr.name)
525 .map(|a| &a.value)
526 .unwrap_or(&attr.value);
527 write_attr_value(&mut writer, var_name, i, true, value)?;
528 }
529 }
530 } else {
531 write_var_data(&mut writer, &self.frames[0].data)?;
532 writer
533 .write_var_i32("uniqueId", &[first.unique_id])
534 .map_err(map_write)?;
535 writer
536 .write_var_f64("timeStamp", &[first.time_stamp])
537 .map_err(map_write)?;
538 writer
539 .write_var_i32("epicsTSSec", &[first.epics_ts_sec])
540 .map_err(map_write)?;
541 writer
542 .write_var_i32("epicsTSNsec", &[first.epics_ts_nsec])
543 .map_err(map_write)?;
544 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
545 write_attr_value(&mut writer, var_name, 0, false, &attr.value)?;
546 }
547 }
548
549 writer.close().map_err(map_write)?;
550 self.frames.clear();
551 Ok(())
552 }
553
554 fn read_file(&mut self) -> ADResult<NDArray> {
555 let path = self
556 .current_path
557 .as_ref()
558 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
559
560 let map_read = |e: netcdf3::error::ReadError| {
561 ADError::UnsupportedConversion(format!("NetCDF read error: {:?}", e))
562 };
563
564 let mut reader = FileReader::open(path).map_err(map_read)?;
565
566 let (is_record, dims, original_type_ordinal) = {
568 let ds = reader.data_set();
569 let var = ds.get_var(VAR_NAME).ok_or_else(|| {
570 ADError::UnsupportedConversion(format!(
571 "variable '{}' not found in NetCDF file",
572 VAR_NAME
573 ))
574 })?;
575
576 let is_record = ds.is_record_var(VAR_NAME).unwrap_or(false);
577
578 let var_dims_rc = var.get_dims();
579 let mut dims: Vec<NDDimension> = Vec::new();
580 for d in &var_dims_rc {
581 if d.is_unlimited() || d.name() == DIM_UNLIMITED {
585 continue;
586 }
587 dims.push(NDDimension::new(d.size()));
588 }
589
590 let original_type_ordinal = ds
591 .get_global_attr_i32("dataType")
592 .and_then(|slice| slice.first().copied());
593
594 (is_record, dims, original_type_ordinal)
595 };
596
597 let data_vec = if is_record {
599 reader.read_record(VAR_NAME, 0).map_err(map_read)?
600 } else {
601 reader.read_var(VAR_NAME).map_err(map_read)?
602 };
603
604 let (nd_type, buf) = match data_vec {
605 netcdf3::DataVector::I8(v) => (NDDataType::Int8, NDDataBuffer::I8(v)),
606 netcdf3::DataVector::U8(v) => (NDDataType::UInt8, NDDataBuffer::U8(v)),
607 netcdf3::DataVector::I16(v) => (NDDataType::Int16, NDDataBuffer::I16(v)),
608 netcdf3::DataVector::I32(v) => (NDDataType::Int32, NDDataBuffer::I32(v)),
609 netcdf3::DataVector::F32(v) => (NDDataType::Float32, NDDataBuffer::F32(v)),
610 netcdf3::DataVector::F64(v) => (NDDataType::Float64, NDDataBuffer::F64(v)),
611 };
612
613 let actual_type = original_type_ordinal
615 .and_then(|v| NDDataType::from_ordinal(v as u8))
616 .unwrap_or(nd_type);
617
618 let buf = match (actual_type, buf) {
620 (NDDataType::UInt16, NDDataBuffer::I16(v)) => {
621 NDDataBuffer::U16(v.into_iter().map(|x| x as u16).collect())
622 }
623 (NDDataType::UInt32, NDDataBuffer::I32(v)) => {
624 NDDataBuffer::U32(v.into_iter().map(|x| x as u32).collect())
625 }
626 (_, buf) => buf,
627 };
628
629 let mut arr = NDArray::new(dims, actual_type);
630 arr.data = buf;
631 Ok(arr)
632 }
633
634 fn supports_multiple_arrays(&self) -> bool {
635 true
636 }
637}
638
639pub struct NetcdfFileProcessor {
641 ctrl: FilePluginController<NetcdfWriter>,
642}
643
644impl NetcdfFileProcessor {
645 pub fn new() -> Self {
646 Self {
647 ctrl: FilePluginController::new(NetcdfWriter::new()),
648 }
649 }
650}
651
652impl Default for NetcdfFileProcessor {
653 fn default() -> Self {
654 Self::new()
655 }
656}
657
658impl NDPluginProcess for NetcdfFileProcessor {
659 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
660 self.ctrl.process_array(array)
661 }
662
663 fn plugin_type(&self) -> &str {
664 "NDFileNetCDF"
665 }
666
667 fn register_params(
668 &mut self,
669 base: &mut asyn_rs::port::PortDriverBase,
670 ) -> asyn_rs::error::AsynResult<()> {
671 self.ctrl.register_params(base)
672 }
673
674 fn on_param_change(
675 &mut self,
676 reason: usize,
677 params: &PluginParamSnapshot,
678 ) -> ParamChangeResult {
679 self.ctrl.on_param_change(reason, params)
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
687 use std::sync::atomic::{AtomicU32, Ordering};
688
689 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
690
691 fn temp_path(prefix: &str) -> PathBuf {
692 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
693 std::env::temp_dir().join(format!("adcore_test_{}_{}.nc", prefix, n))
694 }
695
696 #[test]
697 fn test_write_u8_mono() {
698 let path = temp_path("nc_u8");
699 let mut writer = NetcdfWriter::new();
700
701 let mut arr = NDArray::new(
702 vec![NDDimension::new(4), NDDimension::new(4)],
703 NDDataType::UInt8,
704 );
705 if let NDDataBuffer::U8(v) = &mut arr.data {
706 for i in 0..16 {
707 v[i] = i as u8;
708 }
709 }
710
711 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
712 writer.write_file(&arr).unwrap();
713 writer.close_file().unwrap();
714
715 let data = std::fs::read(&path).unwrap();
717 assert!(data.len() > 16);
718 assert_eq!(&data[0..3], b"CDF", "Expected NetCDF magic bytes");
719
720 std::fs::remove_file(&path).ok();
721 }
722
723 #[test]
724 fn test_write_u16() {
725 let path = temp_path("nc_u16");
726 let mut writer = NetcdfWriter::new();
727
728 let mut arr = NDArray::new(
729 vec![NDDimension::new(4), NDDimension::new(4)],
730 NDDataType::UInt16,
731 );
732 if let NDDataBuffer::U16(v) = &mut arr.data {
733 for i in 0..16 {
734 v[i] = (i * 1000) as u16;
735 }
736 }
737
738 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
739 writer.write_file(&arr).unwrap();
740 writer.close_file().unwrap();
741
742 let data = std::fs::read(&path).unwrap();
743 assert!(data.len() > 32);
744 assert_eq!(&data[0..3], b"CDF");
745
746 std::fs::remove_file(&path).ok();
747 }
748
749 #[test]
750 fn test_roundtrip_u8() {
751 let path = temp_path("nc_rt_u8");
752 let mut writer = NetcdfWriter::new();
753
754 let mut arr = NDArray::new(
755 vec![NDDimension::new(4), NDDimension::new(4)],
756 NDDataType::UInt8,
757 );
758 if let NDDataBuffer::U8(v) = &mut arr.data {
759 for i in 0..16 {
760 v[i] = (i * 10) as u8;
761 }
762 }
763
764 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
765 writer.write_file(&arr).unwrap();
766 writer.close_file().unwrap();
767
768 writer.current_path = Some(path.clone());
769 let read_back = writer.read_file().unwrap();
770 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
771 assert_eq!(orig, read);
772 } else {
773 panic!("data type mismatch on roundtrip");
774 }
775
776 std::fs::remove_file(&path).ok();
777 }
778
779 #[test]
780 fn test_roundtrip_i16() {
781 let path = temp_path("nc_rt_i16");
782 let mut writer = NetcdfWriter::new();
783
784 let mut arr = NDArray::new(
785 vec![NDDimension::new(4), NDDimension::new(4)],
786 NDDataType::Int16,
787 );
788 if let NDDataBuffer::I16(v) = &mut arr.data {
789 for i in 0..16 {
790 v[i] = (i as i16) * 100 - 500;
791 }
792 }
793
794 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
795 writer.write_file(&arr).unwrap();
796 writer.close_file().unwrap();
797
798 writer.current_path = Some(path.clone());
799 let read_back = writer.read_file().unwrap();
800 if let (NDDataBuffer::I16(orig), NDDataBuffer::I16(read)) = (&arr.data, &read_back.data) {
801 assert_eq!(orig, read);
802 } else {
803 panic!("data type mismatch on roundtrip");
804 }
805
806 std::fs::remove_file(&path).ok();
807 }
808
809 #[test]
810 fn test_roundtrip_f32() {
811 let path = temp_path("nc_rt_f32");
812 let mut writer = NetcdfWriter::new();
813
814 let mut arr = NDArray::new(
815 vec![NDDimension::new(4), NDDimension::new(4)],
816 NDDataType::Float32,
817 );
818 if let NDDataBuffer::F32(v) = &mut arr.data {
819 for i in 0..16 {
820 v[i] = i as f32 * 0.5;
821 }
822 }
823
824 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
825 writer.write_file(&arr).unwrap();
826 writer.close_file().unwrap();
827
828 writer.current_path = Some(path.clone());
829 let read_back = writer.read_file().unwrap();
830 if let (NDDataBuffer::F32(orig), NDDataBuffer::F32(read)) = (&arr.data, &read_back.data) {
831 assert_eq!(orig, read);
832 } else {
833 panic!("data type mismatch on roundtrip");
834 }
835
836 std::fs::remove_file(&path).ok();
837 }
838
839 #[test]
840 fn test_multiple_frames() {
841 let path = temp_path("nc_multi");
842 let mut writer = NetcdfWriter::new();
843
844 let mut arr1 = NDArray::new(
845 vec![NDDimension::new(4), NDDimension::new(4)],
846 NDDataType::UInt8,
847 );
848 if let NDDataBuffer::U8(v) = &mut arr1.data {
849 for i in 0..16 {
850 v[i] = i as u8;
851 }
852 }
853
854 let mut arr2 = NDArray::new(
855 vec![NDDimension::new(4), NDDimension::new(4)],
856 NDDataType::UInt8,
857 );
858 if let NDDataBuffer::U8(v) = &mut arr2.data {
859 for i in 0..16 {
860 v[i] = (i as u8).wrapping_add(100);
861 }
862 }
863
864 let mut arr3 = NDArray::new(
865 vec![NDDimension::new(4), NDDimension::new(4)],
866 NDDataType::UInt8,
867 );
868 if let NDDataBuffer::U8(v) = &mut arr3.data {
869 for i in 0..16 {
870 v[i] = (i as u8).wrapping_add(200);
871 }
872 }
873
874 writer.open_file(&path, NDFileMode::Stream, &arr1).unwrap();
875 writer.write_file(&arr1).unwrap();
876 writer.write_file(&arr2).unwrap();
877 writer.write_file(&arr3).unwrap();
878 writer.close_file().unwrap();
879
880 writer.current_path = Some(path.clone());
882 let read_back = writer.read_file().unwrap();
883 if let NDDataBuffer::U8(v) = &read_back.data {
884 assert_eq!(v.len(), 16);
885 for i in 0..16 {
886 assert_eq!(v[i], i as u8, "mismatch at index {}", i);
887 }
888 } else {
889 panic!("expected U8 data");
890 }
891
892 std::fs::remove_file(&path).ok();
893 }
894
895 #[test]
896 fn test_attributes_stored_as_per_frame_variables() {
897 let path = temp_path("nc_attrs");
898 let mut writer = NetcdfWriter::new();
899
900 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
901 arr.attributes.add(NDAttribute::new_static(
902 "exposure",
903 "Exposure time",
904 NDAttrSource::Driver,
905 NDAttrValue::Float64(0.5),
906 ));
907 arr.attributes.add(NDAttribute::new_static(
908 "gain",
909 "Detector gain",
910 NDAttrSource::Driver,
911 NDAttrValue::Int32(42),
912 ));
913
914 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
915 writer.write_file(&arr).unwrap();
916 writer.close_file().unwrap();
917
918 let mut reader = FileReader::open(&path).unwrap();
919 {
920 let ds = reader.data_set();
921 assert!(ds.get_var("Attr_exposure").is_some());
923 assert!(ds.get_var("Attr_gain").is_some());
924 assert_eq!(
926 ds.get_global_attr_as_string("Attr_exposure_DataType"),
927 Some("Float64".to_string())
928 );
929 assert_eq!(
930 ds.get_global_attr_as_string("Attr_gain_DataType"),
931 Some("Int32".to_string())
932 );
933 assert_eq!(
934 ds.get_global_attr_as_string("Attr_exposure_Description"),
935 Some("Exposure time".to_string())
936 );
937 assert_eq!(
938 ds.get_global_attr_as_string("Attr_gain_SourceType"),
939 Some("NDAttrSourceDriver".to_string())
940 );
941 }
942 if let netcdf3::DataVector::F64(v) = reader.read_var("Attr_exposure").unwrap() {
944 assert_eq!(v, vec![0.5]);
945 } else {
946 panic!("Attr_exposure should be F64");
947 }
948 if let netcdf3::DataVector::I32(v) = reader.read_var("Attr_gain").unwrap() {
949 assert_eq!(v, vec![42]);
950 } else {
951 panic!("Attr_gain should be I32");
952 }
953
954 drop(reader);
955 std::fs::remove_file(&path).ok();
956 }
957
958 #[test]
959 fn test_single_frame_array_data_has_leading_numarrays_dim() {
960 let path = temp_path("nc_rank");
961 let mut writer = NetcdfWriter::new();
962
963 let arr = NDArray::new(
964 vec![NDDimension::new(4), NDDimension::new(3)],
965 NDDataType::UInt8,
966 );
967 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
968 writer.write_file(&arr).unwrap();
969 writer.close_file().unwrap();
970
971 let reader = FileReader::open(&path).unwrap();
972 let ds = reader.data_set();
973 let var = ds.get_var("array_data").unwrap();
974 assert_eq!(var.get_dims().len(), 3);
977 assert_eq!(var.get_dims()[0].name(), "numArrays");
978 assert_eq!(var.get_dims()[0].size(), 1);
979
980 drop(reader);
981 std::fs::remove_file(&path).ok();
982 }
983
984 #[test]
985 fn test_all_four_metadata_variables_written_single_frame() {
986 let path = temp_path("nc_meta");
987 let mut writer = NetcdfWriter::new();
988
989 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
990 arr.unique_id = 99;
991 arr.time_stamp = 12.5;
992 arr.timestamp.sec = 555;
993 arr.timestamp.nsec = 777;
994
995 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
996 writer.write_file(&arr).unwrap();
997 writer.close_file().unwrap();
998
999 let mut reader = FileReader::open(&path).unwrap();
1000 for name in ["uniqueId", "timeStamp", "epicsTSSec", "epicsTSNsec"] {
1001 assert!(
1002 reader.data_set().get_var(name).is_some(),
1003 "{name} variable missing"
1004 );
1005 }
1006 match reader.read_var("uniqueId").unwrap() {
1007 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![99]),
1008 other => panic!("uniqueId wrong type: {other:?}"),
1009 }
1010 match reader.read_var("epicsTSSec").unwrap() {
1011 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![555]),
1012 other => panic!("epicsTSSec wrong type: {other:?}"),
1013 }
1014 match reader.read_var("epicsTSNsec").unwrap() {
1015 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![777]),
1016 other => panic!("epicsTSNsec wrong type: {other:?}"),
1017 }
1018
1019 drop(reader);
1020 std::fs::remove_file(&path).ok();
1021 }
1022
1023 #[test]
1024 fn test_nddatatype_ordinals_match_c() {
1025 assert_eq!(NDDataType::Int8 as i32, 0);
1029 assert_eq!(NDDataType::UInt8 as i32, 1);
1030 assert_eq!(NDDataType::Int16 as i32, 2);
1031 assert_eq!(NDDataType::UInt16 as i32, 3);
1032 assert_eq!(NDDataType::Int32 as i32, 4);
1033 assert_eq!(NDDataType::UInt32 as i32, 5);
1034 assert_eq!(NDDataType::Int64 as i32, 6);
1035 assert_eq!(NDDataType::UInt64 as i32, 7);
1036 assert_eq!(NDDataType::Float32 as i32, 8);
1037 assert_eq!(NDDataType::Float64 as i32, 9);
1038 }
1039}