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