1use crate::error::{DatasetsError, Result};
28use netcdf3::{DataSet, DataType, DataVector, FileReader, FileWriter, Version};
29use scirs2_core::ndarray::Array1;
30use std::path::Path;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct NetCdfDimension {
39 pub name: String,
41 pub size: Option<usize>,
44}
45
46#[derive(Debug, Clone)]
48pub enum AttrValue {
49 Byte(Vec<i8>),
51 UByte(Vec<u8>),
53 Short(Vec<i16>),
55 Int(Vec<i32>),
57 Float(Vec<f32>),
59 Double(Vec<f64>),
61 Char(String),
63}
64
65impl AttrValue {
66 fn from_attribute(attr: &netcdf3::Attribute) -> Self {
67 let dt = attr.data_type();
69 match dt {
70 DataType::I8 => attr
71 .get_i8()
72 .map(|s| AttrValue::Byte(s.to_vec()))
73 .unwrap_or(AttrValue::Byte(vec![])),
74 DataType::U8 => {
75 if let Some(s) = attr.get_as_string() {
77 AttrValue::Char(s)
78 } else {
79 attr.get_u8()
80 .map(|s| AttrValue::UByte(s.to_vec()))
81 .unwrap_or(AttrValue::UByte(vec![]))
82 }
83 }
84 DataType::I16 => attr
85 .get_i16()
86 .map(|s| AttrValue::Short(s.to_vec()))
87 .unwrap_or(AttrValue::Short(vec![])),
88 DataType::I32 => attr
89 .get_i32()
90 .map(|s| AttrValue::Int(s.to_vec()))
91 .unwrap_or(AttrValue::Int(vec![])),
92 DataType::F32 => attr
93 .get_f32()
94 .map(|s| AttrValue::Float(s.to_vec()))
95 .unwrap_or(AttrValue::Float(vec![])),
96 DataType::F64 => attr
97 .get_f64()
98 .map(|s| AttrValue::Double(s.to_vec()))
99 .unwrap_or(AttrValue::Double(vec![])),
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct NetCdfAttribute {
107 pub name: String,
109 pub value: AttrValue,
111}
112
113#[derive(Debug, Clone)]
115pub enum NcData {
116 Float(Array1<f32>),
118 Double(Array1<f64>),
120 Int(Array1<i32>),
122 Short(Array1<i16>),
124 Byte(Vec<i8>),
126 UByte(Vec<u8>),
128}
129
130impl NcData {
131 fn from_data_vector(dv: DataVector) -> Self {
132 match dv {
133 DataVector::F32(v) => NcData::Float(Array1::from_vec(v)),
134 DataVector::F64(v) => NcData::Double(Array1::from_vec(v)),
135 DataVector::I32(v) => NcData::Int(Array1::from_vec(v)),
136 DataVector::I16(v) => NcData::Short(Array1::from_vec(v)),
137 DataVector::I8(v) => NcData::Byte(v),
138 DataVector::U8(v) => NcData::UByte(v),
139 }
140 }
141
142 pub fn as_float_array(&self) -> Option<&Array1<f32>> {
146 if let NcData::Float(arr) = self {
147 Some(arr)
148 } else {
149 None
150 }
151 }
152
153 pub fn as_double_array(&self) -> Option<&Array1<f64>> {
155 if let NcData::Double(arr) = self {
156 Some(arr)
157 } else {
158 None
159 }
160 }
161
162 pub fn len(&self) -> usize {
164 match self {
165 NcData::Float(a) => a.len(),
166 NcData::Double(a) => a.len(),
167 NcData::Int(a) => a.len(),
168 NcData::Short(a) => a.len(),
169 NcData::Byte(v) => v.len(),
170 NcData::UByte(v) => v.len(),
171 }
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.len() == 0
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct NetCdfVariable {
183 pub name: String,
185 pub dimensions: Vec<String>,
187 pub attributes: Vec<NetCdfAttribute>,
189 pub dtype: DataType,
191 pub data: NcData,
193}
194
195#[derive(Debug, Clone)]
197pub struct NetCdfDataset {
198 pub dimensions: Vec<NetCdfDimension>,
200 pub global_attributes: Vec<NetCdfAttribute>,
202 pub variables: Vec<NetCdfVariable>,
204}
205
206impl NetCdfDataset {
211 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
218 let path = path.as_ref();
219 if !path.exists() {
220 return Err(DatasetsError::NotFound(format!(
221 "NetCDF file not found: {}",
222 path.display()
223 )));
224 }
225
226 let mut reader = FileReader::open(path)
227 .map_err(|e| DatasetsError::InvalidFormat(format!("NetCDF3 open error: {e:?}")))?;
228
229 Self::from_reader(&mut reader)
230 }
231
232 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
245 use std::io::Write;
246
247 let dir = tempfile::tempdir().map_err(DatasetsError::IoError)?;
248 let path = dir.path().join("from_bytes.nc");
249
250 std::fs::File::create(&path)
251 .map_err(DatasetsError::IoError)?
252 .write_all(bytes)
253 .map_err(DatasetsError::IoError)?;
254
255 let mut reader = FileReader::open(&path)
256 .map_err(|e| DatasetsError::InvalidFormat(format!("NetCDF3 parse error: {e:?}")))?;
257
258 Self::from_reader(&mut reader)
259 }
260
261 fn from_reader(reader: &mut FileReader) -> Result<Self> {
263 let ds: &DataSet = reader.data_set();
264
265 let dimensions: Vec<NetCdfDimension> = ds
267 .get_dims()
268 .iter()
269 .map(|dim| NetCdfDimension {
270 name: dim.name(),
271 size: ds.dim_size(&dim.name()),
272 })
273 .collect();
274
275 let global_attributes: Vec<NetCdfAttribute> = ds
277 .get_global_attrs()
278 .iter()
279 .map(|attr| NetCdfAttribute {
280 name: attr.name().to_owned(),
281 value: AttrValue::from_attribute(attr),
282 })
283 .collect();
284
285 let var_names: Vec<String> = ds.get_var_names();
287 let var_meta: Vec<(String, Vec<String>, Vec<NetCdfAttribute>, DataType)> = var_names
289 .iter()
290 .filter_map(|var_name| {
291 let var_def = ds.get_var(var_name)?;
292 let dim_names: Vec<String> = var_def.dim_names();
293 let attrs: Vec<NetCdfAttribute> = ds
294 .get_var_attrs(var_name)
295 .unwrap_or_default()
296 .iter()
297 .map(|attr| NetCdfAttribute {
298 name: attr.name().to_owned(),
299 value: AttrValue::from_attribute(attr),
300 })
301 .collect();
302 let dtype = var_def.data_type();
303 Some((var_name.clone(), dim_names, attrs, dtype))
304 })
305 .collect();
306
307 let var_data_map = reader
309 .read_all_vars()
310 .map_err(|e| DatasetsError::InvalidFormat(format!("NetCDF3 read vars error: {e:?}")))?;
311
312 let mut variables: Vec<NetCdfVariable> = Vec::with_capacity(var_meta.len());
314 for (var_name, dim_names, attrs, dtype) in var_meta {
315 let data_vec = var_data_map
316 .get(&var_name)
317 .ok_or_else(|| DatasetsError::NotFound(format!("Data for '{var_name}' missing")))?
318 .clone();
319
320 let data = NcData::from_data_vector(data_vec);
321
322 variables.push(NetCdfVariable {
323 name: var_name,
324 dimensions: dim_names,
325 attributes: attrs,
326 dtype,
327 data,
328 });
329 }
330
331 Ok(Self {
332 dimensions,
333 global_attributes,
334 variables,
335 })
336 }
337
338 pub fn variable(&self, name: &str) -> Option<&NetCdfVariable> {
342 self.variables.iter().find(|v| v.name == name)
343 }
344
345 pub fn dimension(&self, name: &str) -> Option<&NetCdfDimension> {
347 self.dimensions.iter().find(|d| d.name == name)
348 }
349
350 pub fn variable_names(&self) -> Vec<&str> {
352 self.variables.iter().map(|v| v.name.as_str()).collect()
353 }
354
355 pub fn dimension_names(&self) -> Vec<&str> {
357 self.dimensions.iter().map(|d| d.name.as_str()).collect()
358 }
359
360 pub fn to_float_array(&self, var_name: &str) -> Result<Array1<f32>> {
367 let var = self
368 .variable(var_name)
369 .ok_or_else(|| DatasetsError::NotFound(format!("Variable '{var_name}' not found")))?;
370 match &var.data {
371 NcData::Float(arr) => Ok(arr.clone()),
372 _ => Err(DatasetsError::InvalidFormat(format!(
373 "Variable '{var_name}' is not Float32 (actual dtype: {:?})",
374 var.dtype
375 ))),
376 }
377 }
378
379 pub fn to_double_array(&self, var_name: &str) -> Result<Array1<f64>> {
386 let var = self
387 .variable(var_name)
388 .ok_or_else(|| DatasetsError::NotFound(format!("Variable '{var_name}' not found")))?;
389 match &var.data {
390 NcData::Double(arr) => Ok(arr.clone()),
391 _ => Err(DatasetsError::InvalidFormat(format!(
392 "Variable '{var_name}' is not Float64 (actual dtype: {:?})",
393 var.dtype
394 ))),
395 }
396 }
397
398 pub fn to_f64_array(&self, var_name: &str) -> Result<Array1<f64>> {
406 let var = self
407 .variable(var_name)
408 .ok_or_else(|| DatasetsError::NotFound(format!("Variable '{var_name}' not found")))?;
409 match &var.data {
410 NcData::Float(a) => Ok(a.mapv(|v| v as f64)),
411 NcData::Double(a) => Ok(a.clone()),
412 NcData::Int(a) => Ok(a.mapv(|v| v as f64)),
413 NcData::Short(a) => Ok(a.mapv(|v| v as f64)),
414 _ => Err(DatasetsError::InvalidFormat(format!(
415 "Variable '{var_name}' cannot be cast to f64 (dtype: {:?})",
416 var.dtype
417 ))),
418 }
419 }
420}
421
422#[doc(hidden)]
433pub fn write_test_nc3_bytes(
434 dim_name: &str,
435 dim_size: usize,
436 var_name: &str,
437 data: &[f32],
438) -> Result<Vec<u8>> {
439 use std::io::Read;
440
441 let dir = tempfile::tempdir().map_err(DatasetsError::IoError)?;
442 let path = dir.path().join("test.nc");
443
444 let mut dataset = DataSet::new();
445 dataset
446 .add_fixed_dim(dim_name, dim_size)
447 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 dim error: {e:?}")))?;
448 dataset
449 .add_var_f32(var_name, &[dim_name])
450 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 var error: {e:?}")))?;
451
452 let mut writer = FileWriter::open(&path)
453 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 writer error: {e:?}")))?;
454 writer
455 .set_def(&dataset, Version::Classic, 0)
456 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 set_def error: {e:?}")))?;
457 writer
458 .write_var_f32(var_name, data)
459 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 write error: {e:?}")))?;
460 writer
461 .close()
462 .map_err(|e| DatasetsError::InvalidFormat(format!("NC3 close error: {e:?}")))?;
463
464 let mut bytes = Vec::new();
465 std::fs::File::open(&path)
466 .map_err(DatasetsError::IoError)?
467 .read_to_end(&mut bytes)
468 .map_err(DatasetsError::IoError)?;
469
470 Ok(bytes)
471}
472
473#[cfg(test)]
478mod tests {
479 use super::*;
480
481 fn make_nc3_file_f32(
483 dim_name: &str,
484 dim_size: usize,
485 var_name: &str,
486 data: &[f32],
487 ) -> (tempfile::TempDir, std::path::PathBuf) {
488 let dir = tempfile::tempdir().expect("tmpdir");
489 let path = dir.path().join("test.nc");
490
491 let mut dataset = DataSet::new();
492 dataset.add_fixed_dim(dim_name, dim_size).expect("add_dim");
493 dataset.add_var_f32(var_name, &[dim_name]).expect("add_var");
494
495 let mut writer = FileWriter::open(&path).expect("writer open");
496 writer
497 .set_def(&dataset, Version::Classic, 0)
498 .expect("set_def");
499 writer.write_var_f32(var_name, data).expect("write_var");
500 writer.close().expect("close");
501
502 (dir, path)
503 }
504
505 fn make_nc3_file_f64(
507 dim_name: &str,
508 dim_size: usize,
509 var_name: &str,
510 data: &[f64],
511 ) -> (tempfile::TempDir, std::path::PathBuf) {
512 let dir = tempfile::tempdir().expect("tmpdir");
513 let path = dir.path().join("test.nc");
514
515 let mut dataset = DataSet::new();
516 dataset.add_fixed_dim(dim_name, dim_size).expect("add_dim");
517 dataset.add_var_f64(var_name, &[dim_name]).expect("add_var");
518
519 let mut writer = FileWriter::open(&path).expect("writer open");
520 writer
521 .set_def(&dataset, Version::Classic, 0)
522 .expect("set_def");
523 writer.write_var_f64(var_name, data).expect("write_var");
524 writer.close().expect("close");
525
526 (dir, path)
527 }
528
529 #[test]
530 fn test_from_file_f32_roundtrip() {
531 let data = vec![1.0_f32, 2.5, std::f32::consts::PI, -1.0];
532 let (_dir, path) = make_nc3_file_f32("time", 4, "temperature", &data);
533
534 let ds = NetCdfDataset::from_file(&path).expect("from_file");
535
536 assert_eq!(ds.variable_names(), vec!["temperature"]);
537 assert_eq!(ds.dimension_names(), vec!["time"]);
538
539 let arr = ds.to_float_array("temperature").expect("to_float_array");
540 assert_eq!(arr.len(), 4);
541 assert!((arr[0] - 1.0).abs() < 1e-6);
542 assert!((arr[2] - std::f32::consts::PI).abs() < 1e-6);
543 }
544
545 #[test]
546 fn test_from_file_f64_roundtrip() {
547 let data = vec![100.0_f64, 200.0, 300.5];
548 let (_dir, path) = make_nc3_file_f64("x", 3, "altitude", &data);
549
550 let ds = NetCdfDataset::from_file(&path).expect("from_file");
551 let arr = ds.to_double_array("altitude").expect("to_double_array");
552
553 assert_eq!(arr.len(), 3);
554 assert!((arr[1] - 200.0).abs() < 1e-12);
555 }
556
557 #[test]
558 fn test_dimension_lookup() {
559 let data = vec![0.0_f32; 5];
560 let (_dir, path) = make_nc3_file_f32("lat", 5, "temp", &data);
561
562 let ds = NetCdfDataset::from_file(&path).expect("from_file");
563 let dim = ds.dimension("lat").expect("dimension lat");
564 assert_eq!(dim.name, "lat");
565 assert_eq!(dim.size, Some(5));
566 }
567
568 #[test]
569 fn test_variable_not_found() {
570 let data = vec![1.0_f32];
571 let (_dir, path) = make_nc3_file_f32("d", 1, "v", &data);
572
573 let ds = NetCdfDataset::from_file(&path).expect("from_file");
574 let result = ds.to_float_array("nonexistent");
575 assert!(result.is_err());
576 }
577
578 #[test]
579 fn test_from_file_not_found() {
580 let result = NetCdfDataset::from_file("/tmp/__scirs2_nonexistent_9999.nc");
581 assert!(matches!(result, Err(DatasetsError::NotFound(_))));
582 }
583
584 #[test]
585 fn test_from_bytes_roundtrip() {
586 let data = vec![10.0_f32, 20.0, 30.0];
587 let bytes = write_test_nc3_bytes("x", 3, "signal", &data).expect("write bytes");
588
589 assert!(!bytes.is_empty());
591 assert_eq!(&bytes[0..3], b"CDF");
592
593 let ds = NetCdfDataset::from_bytes(&bytes).expect("from_bytes");
594 let arr = ds.to_float_array("signal").expect("to_float_array");
595
596 assert_eq!(arr.len(), 3);
597 assert!((arr[0] - 10.0).abs() < 1e-6);
598 assert!((arr[2] - 30.0).abs() < 1e-6);
599 }
600
601 #[test]
602 fn test_to_f64_array_from_f32_variable() {
603 let data = vec![1.5_f32, 2.5, 3.5];
604 let (_dir, path) = make_nc3_file_f32("n", 3, "values", &data);
605
606 let ds = NetCdfDataset::from_file(&path).expect("from_file");
607 let arr = ds.to_f64_array("values").expect("to_f64_array");
608
609 assert_eq!(arr.len(), 3);
610 assert!((arr[0] - 1.5).abs() < 1e-5);
611 }
612
613 #[test]
614 fn test_variable_dim_references() {
615 let data = vec![0.0_f32; 4];
616 let (_dir, path) = make_nc3_file_f32("time", 4, "u_wind", &data);
617
618 let ds = NetCdfDataset::from_file(&path).expect("from_file");
619 let var = ds.variable("u_wind").expect("variable u_wind");
620 assert_eq!(var.dimensions, vec!["time"]);
621 assert_eq!(var.dtype, DataType::F32);
622 }
623
624 #[test]
625 fn test_nc3_magic_bytes() {
626 let data = vec![0.0_f32; 1];
627 let bytes = write_test_nc3_bytes("d", 1, "v", &data).expect("write");
628 assert_eq!(bytes[0], b'C');
630 assert_eq!(bytes[1], b'D');
631 assert_eq!(bytes[2], b'F');
632 assert_eq!(bytes[3], 0x01);
633 }
634
635 #[test]
636 fn test_global_attribute_reading() {
637 let dir = tempfile::tempdir().expect("tmpdir");
638 let path = dir.path().join("with_attr.nc");
639
640 let mut dataset = DataSet::new();
641 dataset.add_fixed_dim("t", 2).expect("add_dim");
642 dataset.add_var_f32("temp", &["t"]).expect("add_var");
643 dataset
644 .add_global_attr_string("institution", "Test Institute")
645 .expect("add_attr");
646
647 let mut writer = FileWriter::open(&path).expect("open");
648 writer
649 .set_def(&dataset, Version::Classic, 0)
650 .expect("set_def");
651 writer.write_var_f32("temp", &[1.0, 2.0]).expect("write");
652 writer.close().expect("close");
653
654 let ds = NetCdfDataset::from_file(&path).expect("from_file");
655 assert!(!ds.global_attributes.is_empty());
656
657 let inst = ds
658 .global_attributes
659 .iter()
660 .find(|a| a.name == "institution")
661 .expect("institution attr");
662 if let AttrValue::Char(s) = &inst.value {
663 assert_eq!(s, "Test Institute");
664 } else {
665 if let AttrValue::UByte(bytes) = &inst.value {
667 let decoded = String::from_utf8_lossy(bytes);
668 assert!(decoded.contains("Test Institute"));
669 } else {
670 panic!(
671 "Expected Char or UByte attribute for institution, got: {:?}",
672 inst.value
673 );
674 }
675 }
676 }
677
678 #[test]
679 fn test_variable_attribute_reading() {
680 let dir = tempfile::tempdir().expect("tmpdir");
681 let path = dir.path().join("var_attr.nc");
682
683 let mut dataset = DataSet::new();
684 dataset.add_fixed_dim("z", 3).expect("add_dim");
685 dataset.add_var_f32("pressure", &["z"]).expect("add_var");
686 dataset
687 .add_var_attr_string("pressure", "units", "hPa")
688 .expect("add_attr");
689
690 let mut writer = FileWriter::open(&path).expect("open");
691 writer
692 .set_def(&dataset, Version::Classic, 0)
693 .expect("set_def");
694 writer
695 .write_var_f32("pressure", &[1013.0, 850.0, 500.0])
696 .expect("write");
697 writer.close().expect("close");
698
699 let ds = NetCdfDataset::from_file(&path).expect("from_file");
700 let var = ds.variable("pressure").expect("pressure variable");
701
702 let units_attr = var
703 .attributes
704 .iter()
705 .find(|a| a.name == "units")
706 .expect("units attr");
707 match &units_attr.value {
709 AttrValue::Char(s) => assert_eq!(s, "hPa"),
710 AttrValue::UByte(b) => {
711 let decoded = String::from_utf8_lossy(b);
712 assert!(decoded.contains("hPa"));
713 }
714 other => panic!("Unexpected attribute variant: {:?}", other),
715 }
716 }
717
718 #[test]
719 fn test_i32_variable() {
720 let dir = tempfile::tempdir().expect("tmpdir");
721 let path = dir.path().join("int_var.nc");
722
723 let mut dataset = DataSet::new();
724 dataset.add_fixed_dim("n", 3).expect("add_dim");
725 dataset.add_var_i32("counts", &["n"]).expect("add_var");
726
727 let mut writer = FileWriter::open(&path).expect("open");
728 writer
729 .set_def(&dataset, Version::Classic, 0)
730 .expect("set_def");
731 writer
732 .write_var_i32("counts", &[10, 20, 30])
733 .expect("write");
734 writer.close().expect("close");
735
736 let ds = NetCdfDataset::from_file(&path).expect("from_file");
737 let arr = ds.to_f64_array("counts").expect("to_f64_array");
738
739 assert_eq!(arr.len(), 3);
740 assert!((arr[0] - 10.0).abs() < 1e-12);
741 assert!((arr[2] - 30.0).abs() < 1e-12);
742 }
743}