1use scirs2_core::ndarray::{Array, Array2, ArrayD, Dimension};
20use std::collections::HashMap;
21use std::fs;
22use std::path::Path;
23
24use crate::error::{IoError, Result};
25use crate::hdf5::{AttributeValue as HDF5AttributeValue, FileMode as HDF5FileMode, HDF5File};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum NetCDFDataType {
30 Byte,
32 Char,
34 Short,
36 Int,
38 Float,
40 Double,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum NetCDFFormat {
47 Classic,
49 NetCDF4,
51 NetCDF4Classic,
53}
54
55pub struct NetCDFFile {
57 #[allow(dead_code)]
59 path: String,
60 mode: String,
62 format: NetCDFFormat,
64 dimensions: HashMap<String, Option<usize>>,
66 variables: HashMap<String, VariableInfo>,
68 attributes: HashMap<String, AttributeValue>,
70 hdf5_backend: Option<HDF5File>,
72}
73
74#[derive(Debug, Clone)]
76struct VariableInfo {
77 #[allow(dead_code)]
79 name: String,
80 data_type: NetCDFDataType,
82 dimensions: Vec<String>,
84 attributes: HashMap<String, AttributeValue>,
86}
87
88#[derive(Debug, Clone)]
90#[allow(dead_code)]
91enum AttributeValue {
92 String(String),
94 Byte(i8),
96 Short(i16),
98 Int(i32),
100 Float(f32),
102 Double(f64),
104 ByteArray(Vec<i8>),
106 ShortArray(Vec<i16>),
108 IntArray(Vec<i32>),
110 FloatArray(Vec<f32>),
112 DoubleArray(Vec<f64>),
114}
115
116#[derive(Debug, Clone)]
118pub struct NetCDFOptions {
119 pub mmap: bool,
121 pub auto_scale: bool,
123 pub mask_and_scale: bool,
125 pub mode: String,
127 pub format: NetCDFFormat,
129 pub enable_compression: bool,
131 pub compression_level: Option<u8>,
133 pub enable_chunking: bool,
135}
136
137impl Default for NetCDFOptions {
138 fn default() -> Self {
139 Self {
140 mmap: true,
141 auto_scale: true,
142 mask_and_scale: true,
143 mode: "r".to_string(),
144 format: NetCDFFormat::Classic,
145 enable_compression: false,
146 compression_level: None,
147 enable_chunking: false,
148 }
149 }
150}
151
152impl NetCDFFile {
153 pub fn open<P: AsRef<Path>>(path: P, options: Option<NetCDFOptions>) -> Result<Self> {
179 let opts = options.unwrap_or_default();
180 let path_str = path.as_ref().to_string_lossy().to_string();
181
182 if opts.mode == "r" && !Path::new(&path_str).exists() {
183 return Err(IoError::FileError(format!("File not found: {}", path_str)));
184 }
185
186 let hdf5_backend = if opts.format == NetCDFFormat::NetCDF4
188 || opts.format == NetCDFFormat::NetCDF4Classic
189 {
190 if opts.mode == "r" {
191 Some(HDF5File::open(&path_str, HDF5FileMode::ReadOnly)?)
192 } else {
193 None
194 }
195 } else {
196 None
197 };
198
199 Ok(Self {
202 path: path_str,
203 mode: opts.mode,
204 format: opts.format,
205 dimensions: HashMap::new(),
206 variables: HashMap::new(),
207 attributes: HashMap::new(),
208 hdf5_backend,
209 })
210 }
211
212 pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
222 Self::create_with_format(path, NetCDFFormat::Classic)
223 }
224
225 pub fn create_with_format<P: AsRef<Path>>(path: P, format: NetCDFFormat) -> Result<Self> {
236 let opts = NetCDFOptions {
237 mode: "w".to_string(),
238 format,
239 ..Default::default()
240 };
241
242 let path_str = path.as_ref().to_string_lossy().to_string();
243
244 if let Some(parent) = Path::new(&path_str).parent() {
246 if !parent.exists() {
247 fs::create_dir_all(parent).map_err(|e| {
248 IoError::FileError(format!("Failed to create directories: {}", e))
249 })?;
250 }
251 }
252
253 let hdf5_backend =
255 if format == NetCDFFormat::NetCDF4 || format == NetCDFFormat::NetCDF4Classic {
256 Some(HDF5File::create(&path_str)?)
257 } else {
258 None
259 };
260
261 Ok(Self {
262 path: path_str,
263 mode: opts.mode,
264 format: opts.format,
265 dimensions: HashMap::new(),
266 variables: HashMap::new(),
267 attributes: HashMap::new(),
268 hdf5_backend,
269 })
270 }
271
272 pub fn create_dimension(&mut self, name: &str, size: Option<usize>) -> Result<()> {
283 if self.mode != "w" {
284 return Err(IoError::ValidationError(
285 "File not opened in write mode".to_string(),
286 ));
287 }
288
289 self.dimensions.insert(name.to_string(), size);
290
291 if let Some(ref mut hdf5) = self.hdf5_backend {
293 let dim_attr = format!("_dim_{}", name);
296 let dim_value = match size {
297 Some(s) => s.to_string(),
298 None => "unlimited".to_string(),
299 };
300 hdf5.root_mut()
301 .set_attribute(&dim_attr, HDF5AttributeValue::String(dim_value));
302 }
303
304 Ok(())
305 }
306
307 pub fn create_variable(
319 &mut self,
320 name: &str,
321 data_type: NetCDFDataType,
322 dimensions: &[&str],
323 ) -> Result<()> {
324 if self.mode != "w" {
325 return Err(IoError::ValidationError(
326 "File not opened in write mode".to_string(),
327 ));
328 }
329
330 for &dim in dimensions {
332 if !self.dimensions.contains_key(dim) {
333 return Err(IoError::ValidationError(format!(
334 "Dimension '{}' not defined",
335 dim
336 )));
337 }
338 }
339
340 let var_info = VariableInfo {
341 name: name.to_string(),
342 data_type,
343 dimensions: dimensions.iter().map(|&s| s.to_string()).collect(),
344 attributes: HashMap::new(),
345 };
346
347 self.variables.insert(name.to_string(), var_info);
348
349 if let Some(ref mut hdf5) = self.hdf5_backend {
351 let var_group_path = format!("_var_{}", name);
353 let var_group = hdf5.root_mut().create_group(&var_group_path);
354
355 var_group.set_attribute(
356 "data_type",
357 HDF5AttributeValue::String(format!("{:?}", data_type)),
358 );
359 var_group.set_attribute(
360 "dimensions",
361 HDF5AttributeValue::StringArray(dimensions.iter().map(|s| s.to_string()).collect()),
362 );
363 }
364
365 Ok(())
366 }
367
368 pub fn read_variable<T: Clone + Default + 'static>(&self, name: &str) -> Result<ArrayD<T>> {
383 if self.mode != "r" {
384 return Err(IoError::ValidationError(
385 "File not opened in read mode".to_string(),
386 ));
387 }
388
389 let var_info = self
390 .variables
391 .get(name)
392 .ok_or_else(|| IoError::ValidationError(format!("Variable '{}' not found", name)))?;
393
394 let shape: Vec<usize> = var_info
396 .dimensions
397 .iter()
398 .map(|dim_name| {
399 self.dimensions
400 .get(dim_name)
401 .unwrap_or(&Some(1))
402 .unwrap_or(1)
403 })
404 .collect();
405
406 if let Some(ref hdf5) = self.hdf5_backend {
408 let array_f64 = self.read_compressed_variable_data(hdf5, name)?;
410
411 let data: Vec<T> = self.convert_data_type(&array_f64)?;
413
414 return Array::from_shape_vec(array_f64.shape(), data)
415 .map_err(|e| IoError::FormatError(format!("Failed to create array: {}", e)));
416 }
417
418 let total_size = shape.iter().product();
420 let data = vec![T::default(); total_size];
421
422 Array::from_shape_vec(shape, data)
423 .map_err(|e| IoError::FormatError(format!("Failed to create array: {}", e)))
424 }
425
426 pub fn write_variable<T: Clone + Into<f64> + std::fmt::Debug, D: Dimension>(
437 &mut self,
438 name: &str,
439 data: &Array<T, D>,
440 ) -> Result<()> {
441 if self.mode != "w" {
442 return Err(IoError::ValidationError(
443 "File not opened in write mode".to_string(),
444 ));
445 }
446
447 if !self.variables.contains_key(name) {
448 return Err(IoError::ValidationError(format!(
449 "Variable '{}' not defined",
450 name
451 )));
452 }
453
454 let compression_level = self.get_compression_level();
457 let chunking_enabled = self.is_chunking_enabled();
458 let chunk_size = if chunking_enabled {
459 Some(self.calculate_optimal_chunk_size(data.shape()))
460 } else {
461 None
462 };
463
464 if let Some(ref mut hdf5) = self.hdf5_backend {
465 let mut dataset_options = crate::hdf5::DatasetOptions::default();
467
468 if let Some(level) = compression_level {
470 dataset_options.compression.gzip = Some(level);
471 dataset_options.compression.shuffle = true; }
473
474 if let Some(chunk) = chunk_size {
476 dataset_options.chunk_size = Some(chunk);
477 }
478
479 let has_compression = dataset_options.compression.gzip.is_some();
481
482 hdf5.create_dataset_from_array(name, data, Some(dataset_options))?;
484
485 if has_compression {
487 if let Ok(_dataset) = hdf5.get_dataset(name) {
488 }
490 }
491 } else {
492 }
495
496 Ok(())
497 }
498
499 pub fn add_variable_attribute(
511 &mut self,
512 var_name: &str,
513 attr_name: &str,
514 value: &str,
515 ) -> Result<()> {
516 if self.mode != "w" {
517 return Err(IoError::ValidationError(
518 "File not opened in write mode".to_string(),
519 ));
520 }
521
522 let var_info = self.variables.get_mut(var_name).ok_or_else(|| {
523 IoError::ValidationError(format!("Variable '{}' not defined", var_name))
524 })?;
525
526 var_info.attributes.insert(
527 attr_name.to_string(),
528 AttributeValue::String(value.to_string()),
529 );
530
531 Ok(())
532 }
533
534 fn read_compressed_variable_data(&self, hdf5: &HDF5File, name: &str) -> Result<ArrayD<f64>> {
536 let array_data = hdf5.read_dataset(name)?;
538
539 if let Ok(dataset) = hdf5.get_dataset(name) {
541 let has_compression = dataset.get_attribute("compression").is_some()
543 || dataset.get_attribute("shuffle").is_some()
544 || dataset.get_attribute("deflate").is_some();
545
546 if has_compression {
547 if let Some(chunk_attr) = dataset.get_attribute("chunk_sizes") {
552 self.process_chunked_data(&array_data, chunk_attr)?;
554 }
555 }
556 }
557
558 Ok(array_data)
559 }
560
561 fn process_chunked_data(
563 &self,
564 array_data: &ArrayD<f64>,
565 _chunk_attr: &crate::hdf5::AttributeValue,
566 ) -> Result<()> {
567 let _chunk_info = format!("Processing {} elements in chunked format", array_data.len());
575 Ok(())
578 }
579
580 fn convert_data_type<T>(&self, arrayf64: &ArrayD<f64>) -> Result<Vec<T>>
582 where
583 T: Clone + Default + 'static,
584 {
585 let data: Vec<T> = arrayf64
587 .iter()
588 .map(|&x| {
589 let value: Box<dyn std::any::Any> =
591 if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
592 Box::new(x)
593 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
594 Box::new(x as f32)
595 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i32>() {
596 Box::new(x as i32)
597 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i16>() {
598 Box::new(x as i16)
599 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i8>() {
600 Box::new(x as i8)
601 } else {
602 return T::default();
604 };
605
606 if let Ok(boxed_t) = value.downcast::<T>() {
608 *boxed_t
609 } else {
610 T::default()
611 }
612 })
613 .collect();
614
615 Ok(data)
616 }
617
618 fn get_compression_level(&self) -> Option<u8> {
620 if let Some(AttributeValue::String(level_str)) = self.attributes.get("compression_level") {
622 level_str.parse().ok()
623 } else {
624 match self.format {
626 NetCDFFormat::NetCDF4 | NetCDFFormat::NetCDF4Classic => Some(6), NetCDFFormat::Classic => None, }
629 }
630 }
631
632 fn is_chunking_enabled(&self) -> bool {
634 if let Some(AttributeValue::String(chunking_str)) = self.attributes.get("chunking") {
636 chunking_str.to_lowercase() == "true" || chunking_str == "1"
637 } else {
638 matches!(
640 self.format,
641 NetCDFFormat::NetCDF4 | NetCDFFormat::NetCDF4Classic
642 )
643 }
644 }
645
646 fn calculate_optimal_chunk_size(&self, shape: &[usize]) -> Vec<usize> {
648 const TARGET_CHUNK_BYTES: usize = 1024 * 1024;
650 const ELEMENT_SIZE: usize = 8; let target_elements = TARGET_CHUNK_BYTES / ELEMENT_SIZE;
653
654 if shape.is_empty() {
655 return vec![1];
656 }
657
658 if shape.len() == 1 {
660 let chunk_size = (target_elements).min(shape[0]).max(1);
661 return vec![chunk_size];
662 }
663
664 let total_elements: usize = shape.iter().product();
666
667 if total_elements <= target_elements {
668 return shape.to_vec();
670 }
671
672 let scale_factor =
674 (target_elements as f64 / total_elements as f64).powf(1.0 / shape.len() as f64);
675
676 let mut chunkshape: Vec<usize> = shape
677 .iter()
678 .map(|&dim| ((dim as f64 * scale_factor) as usize).max(1))
679 .collect();
680
681 for (i, &max_dim) in shape.iter().enumerate() {
683 chunkshape[i] = chunkshape[i].min(max_dim);
684 }
685
686 if shape.len() >= 2 {
688 let time_chunk = (target_elements / shape[1..].iter().product::<usize>()).max(1);
689 chunkshape[0] = time_chunk.min(shape[0]);
690 }
691
692 chunkshape
693 }
694
695 pub fn add_global_attribute(&mut self, name: &str, value: &str) -> Result<()> {
706 if self.mode != "w" {
707 return Err(IoError::ValidationError(
708 "File not opened in write mode".to_string(),
709 ));
710 }
711
712 self.attributes
713 .insert(name.to_string(), AttributeValue::String(value.to_string()));
714
715 Ok(())
716 }
717
718 pub fn dimensions(&self) -> &HashMap<String, Option<usize>> {
724 &self.dimensions
725 }
726
727 pub fn variables(&self) -> Vec<String> {
733 self.variables.keys().cloned().collect()
734 }
735
736 pub fn variable_info(
746 &self,
747 name: &str,
748 ) -> Result<(NetCDFDataType, Vec<String>, HashMap<String, String>)> {
749 let var_info = self
750 .variables
751 .get(name)
752 .ok_or_else(|| IoError::ValidationError(format!("Variable '{}' not found", name)))?;
753
754 let mut attributes = HashMap::new();
755 for (attr_name, attr_value) in &var_info.attributes {
756 let value = match attr_value {
757 AttributeValue::String(s) => s.clone(),
758 AttributeValue::Byte(b) => b.to_string(),
759 AttributeValue::Short(s) => s.to_string(),
760 AttributeValue::Int(i) => i.to_string(),
761 AttributeValue::Float(f) => f.to_string(),
762 AttributeValue::Double(d) => d.to_string(),
763 AttributeValue::ByteArray(arr) => format!("{:?}", arr),
764 AttributeValue::ShortArray(arr) => format!("{:?}", arr),
765 AttributeValue::IntArray(arr) => format!("{:?}", arr),
766 AttributeValue::FloatArray(arr) => format!("{:?}", arr),
767 AttributeValue::DoubleArray(arr) => format!("{:?}", arr),
768 };
769 attributes.insert(attr_name.clone(), value);
770 }
771
772 Ok((var_info.data_type, var_info.dimensions.clone(), attributes))
773 }
774
775 pub fn global_attributes(&self) -> HashMap<String, String> {
781 self.attributes
782 .iter()
783 .map(|(name, value)| {
784 let value_str = match value {
785 AttributeValue::String(s) => s.clone(),
786 AttributeValue::Byte(b) => b.to_string(),
787 AttributeValue::Short(s) => s.to_string(),
788 AttributeValue::Int(i) => i.to_string(),
789 AttributeValue::Float(f) => f.to_string(),
790 AttributeValue::Double(d) => d.to_string(),
791 AttributeValue::ByteArray(arr) => format!("{:?}", arr),
792 AttributeValue::ShortArray(arr) => format!("{:?}", arr),
793 AttributeValue::IntArray(arr) => format!("{:?}", arr),
794 AttributeValue::FloatArray(arr) => format!("{:?}", arr),
795 AttributeValue::DoubleArray(arr) => format!("{:?}", arr),
796 };
797 (name.clone(), value_str)
798 })
799 .collect()
800 }
801
802 pub fn format(&self) -> NetCDFFormat {
804 self.format
805 }
806
807 pub fn has_hdf5_backend(&self) -> bool {
809 self.hdf5_backend.is_some()
810 }
811
812 pub fn write_array<T: Clone + Into<f64> + std::fmt::Debug, D: Dimension>(
824 &mut self,
825 name: &str,
826 data: &Array<T, D>,
827 dimension_names: &[&str],
828 ) -> Result<()> {
829 if self.format == NetCDFFormat::Classic {
830 return Err(IoError::ValidationError(
831 "write_array is only supported for NetCDF4/HDF5 format".to_string(),
832 ));
833 }
834
835 for (i, &dim_name) in dimension_names.iter().enumerate() {
837 if !self.dimensions.contains_key(dim_name) {
838 let dim_size = data.shape()[i];
839 self.create_dimension(dim_name, Some(dim_size))?;
840 }
841 }
842
843 if !self.variables.contains_key(name) {
845 self.create_variable(name, NetCDFDataType::Double, dimension_names)?;
846 }
847
848 self.write_variable(name, data)
850 }
851
852 pub fn read_array(&self, name: &str) -> Result<ArrayD<f64>> {
862 if let Some(backend) = &self.hdf5_backend {
863 backend.read_dataset(name)
865 } else {
866 self.read_variable::<f64>(name)
868 }
869 }
870
871 pub fn sync(&mut self) -> Result<()> {
877 if let Some(ref mut hdf5) = self.hdf5_backend {
878 hdf5.write()?;
879 }
880 Ok(())
882 }
883
884 pub fn close(mut self) -> Result<()> {
890 self.sync()?;
891 if let Some(hdf5) = self.hdf5_backend {
892 hdf5.close()?;
893 }
894 Ok(())
895 }
896}
897
898#[allow(dead_code)]
935pub fn create_netcdf4_with_data<P: AsRef<Path>>(
936 path: P,
937 datasets: HashMap<String, (ArrayD<f64>, Vec<String>)>,
938 global_attributes: HashMap<String, String>,
939) -> Result<()> {
940 let mut file = NetCDFFile::create_with_format(path, NetCDFFormat::NetCDF4)?;
941
942 for (name, value) in global_attributes {
944 file.add_global_attribute(&name, &value)?;
945 }
946
947 for (var_name, (data, dim_names)) in datasets {
949 let dim_refs: Vec<&str> = dim_names.iter().map(|s| s.as_str()).collect();
950 file.write_array(&var_name, &data, &dim_refs)?;
951 }
952
953 file.close()
954}
955
956#[allow(dead_code)]
977pub fn read_netcdf<P: AsRef<Path>>(path: P) -> Result<NetCDFFile> {
978 let path_ref = path.as_ref();
979
980 match NetCDFFile::open(
982 path_ref,
983 Some(NetCDFOptions {
984 format: NetCDFFormat::NetCDF4,
985 mode: "r".to_string(),
986 ..Default::default()
987 }),
988 ) {
989 Ok(file) => Ok(file),
990 Err(_) => {
991 NetCDFFile::open(
993 path_ref,
994 Some(NetCDFOptions {
995 format: NetCDFFormat::Classic,
996 mode: "r".to_string(),
997 ..Default::default()
998 }),
999 )
1000 }
1001 }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007
1008 #[test]
1009 fn test_create_netcdf() {
1010 let temp_dir = std::env::temp_dir();
1011 let test_file = temp_dir.join(format!("test_create_netcdf_{}.nc", std::process::id()));
1012 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1013
1014 let file = NetCDFFile::create(test_path).expect("Operation failed");
1015 assert_eq!(file.mode, "w");
1016 assert_eq!(file.path, test_path);
1017 assert!(file.dimensions.is_empty());
1018 assert!(file.variables.is_empty());
1019 assert!(file.attributes.is_empty());
1020
1021 drop(file);
1022 let _ = std::fs::remove_file(test_file);
1023 }
1024
1025 #[test]
1026 fn test_add_dimension() {
1027 let temp_dir = std::env::temp_dir();
1028 let test_file = temp_dir.join(format!("test_add_dimension_{}.nc", std::process::id()));
1029 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1030
1031 let mut file = NetCDFFile::create(test_path).expect("Operation failed");
1032 file.create_dimension("time", Some(10))
1033 .expect("Operation failed");
1034 file.create_dimension("lat", Some(180))
1035 .expect("Operation failed");
1036 file.create_dimension("lon", Some(360))
1037 .expect("Operation failed");
1038 file.create_dimension("unlimited", None)
1039 .expect("Operation failed");
1040
1041 assert_eq!(file.dimensions.len(), 4);
1042 assert_eq!(
1043 *file.dimensions.get("time").expect("Operation failed"),
1044 Some(10)
1045 );
1046 assert_eq!(
1047 *file.dimensions.get("lat").expect("Operation failed"),
1048 Some(180)
1049 );
1050 assert_eq!(
1051 *file.dimensions.get("lon").expect("Operation failed"),
1052 Some(360)
1053 );
1054 assert_eq!(
1055 *file.dimensions.get("unlimited").expect("Operation failed"),
1056 None
1057 );
1058
1059 drop(file);
1060 let _ = std::fs::remove_file(test_file);
1061 }
1062
1063 #[test]
1064 fn test_add_variable() {
1065 let temp_dir = std::env::temp_dir();
1066 let test_file = temp_dir.join(format!("test_add_variable_{}.nc", std::process::id()));
1067 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1068
1069 let mut file = NetCDFFile::create(test_path).expect("Operation failed");
1070 file.create_dimension("time", Some(10))
1071 .expect("Operation failed");
1072 file.create_dimension("lat", Some(180))
1073 .expect("Operation failed");
1074 file.create_dimension("lon", Some(360))
1075 .expect("Operation failed");
1076
1077 file.create_variable(
1078 "temperature",
1079 NetCDFDataType::Float,
1080 &["time", "lat", "lon"],
1081 )
1082 .expect("Operation failed");
1083
1084 assert_eq!(file.variables.len(), 1);
1085 assert!(file.variables.contains_key("temperature"));
1086
1087 let var_info = file.variables.get("temperature").expect("Operation failed");
1088 assert_eq!(var_info.name, "temperature");
1089 assert_eq!(var_info.data_type, NetCDFDataType::Float);
1090 assert_eq!(var_info.dimensions, vec!["time", "lat", "lon"]);
1091
1092 drop(file);
1093 let _ = std::fs::remove_file(test_file);
1094 }
1095
1096 #[test]
1097 fn test_attributes() {
1098 let temp_dir = std::env::temp_dir();
1099 let test_file = temp_dir.join(format!("test_attributes_{}.nc", std::process::id()));
1100 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1101
1102 let mut file = NetCDFFile::create(test_path).expect("Operation failed");
1103 file.create_dimension("x", Some(10))
1104 .expect("Operation failed");
1105 file.create_variable("data", NetCDFDataType::Double, &["x"])
1106 .expect("Operation failed");
1107
1108 file.add_global_attribute("title", "Test Dataset")
1110 .expect("Operation failed");
1111 file.add_global_attribute("author", "SciRS2 Test")
1112 .expect("Operation failed");
1113
1114 let global_attrs = file.global_attributes();
1115 assert!(global_attrs.contains_key("title"));
1116 assert!(global_attrs.contains_key("author"));
1117 assert_eq!(global_attrs["title"], "Test Dataset");
1118 assert_eq!(global_attrs["author"], "SciRS2 Test");
1119
1120 file.add_variable_attribute("data", "units", "meters")
1122 .expect("Operation failed");
1123 file.add_variable_attribute("data", "long_name", "measurement data")
1124 .expect("Operation failed");
1125
1126 let (dtype, dims, var_attrs) = file.variable_info("data").expect("Operation failed");
1127 assert_eq!(dtype, NetCDFDataType::Double);
1128 assert_eq!(dims, vec!["x"]);
1129 assert!(var_attrs.contains_key("units"));
1130 assert!(var_attrs.contains_key("long_name"));
1131 assert_eq!(var_attrs["units"], "meters");
1132 assert_eq!(var_attrs["long_name"], "measurement data");
1133
1134 drop(file);
1135 let _ = std::fs::remove_file(test_file);
1136 }
1137
1138 #[test]
1139 fn test_read_write_variable() {
1140 let temp_dir = std::env::temp_dir();
1141 let test_file = temp_dir.join(format!("test_read_write_{}.nc", std::process::id()));
1142 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1143
1144 let mut file = NetCDFFile::create(test_path).expect("Operation failed");
1146 file.create_dimension("x", Some(3))
1147 .expect("Operation failed");
1148 file.create_dimension("y", Some(2))
1149 .expect("Operation failed");
1150 file.create_variable("data", NetCDFDataType::Float, &["x", "y"])
1151 .expect("Operation failed");
1152
1153 let data = Array2::<f32>::zeros((3, 2));
1155 file.write_variable("data", &data)
1156 .expect("Operation failed");
1157
1158 drop(file);
1159 let _ = std::fs::remove_file(&test_file);
1160
1161 let read_test_file_path = temp_dir.join(format!("test_read_{}.nc", std::process::id()));
1166 let read_test_path = read_test_file_path
1167 .to_str()
1168 .expect("path should be valid UTF-8");
1169
1170 let mut read_test_file = NetCDFFile::create(read_test_path).expect("Operation failed");
1171
1172 read_test_file.mode = "r".to_string();
1174
1175 read_test_file.dimensions.insert("x".to_string(), Some(3));
1177 read_test_file.dimensions.insert("y".to_string(), Some(2));
1178 read_test_file.variables.insert(
1179 "data".to_string(),
1180 VariableInfo {
1181 name: "data".to_string(),
1182 data_type: NetCDFDataType::Float,
1183 dimensions: vec!["x".to_string(), "y".to_string()],
1184 attributes: HashMap::new(),
1185 },
1186 );
1187
1188 let read_data: ArrayD<f32> = read_test_file
1190 .read_variable("data")
1191 .expect("Operation failed");
1192 assert_eq!(read_data.shape(), &[3, 2]);
1193
1194 drop(read_test_file);
1195 let _ = std::fs::remove_file(read_test_file_path);
1196 }
1197
1198 #[test]
1199 fn test_netcdf4_format_creation() {
1200 let temp_dir = std::env::temp_dir();
1201 let test_file = temp_dir.join(format!("test_netcdf4_format_{}.nc", std::process::id()));
1202 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1203
1204 let file = NetCDFFile::create_with_format(test_path, NetCDFFormat::NetCDF4)
1205 .expect("Operation failed");
1206 assert_eq!(file.format(), NetCDFFormat::NetCDF4);
1207 assert!(file.has_hdf5_backend());
1208
1209 drop(file);
1210 let _ = std::fs::remove_file(test_file);
1211 }
1212
1213 #[test]
1214 fn test_netcdf_format_differences() {
1215 let temp_dir = std::env::temp_dir();
1216 let classic_file = temp_dir.join(format!("test_classic_{}.nc", std::process::id()));
1217 let netcdf4_file = temp_dir.join(format!("test_netcdf4_{}.nc", std::process::id()));
1218 let classic_path = classic_file.to_str().expect("path should be valid UTF-8");
1219 let netcdf4_path = netcdf4_file.to_str().expect("path should be valid UTF-8");
1220
1221 let classic = NetCDFFile::create_with_format(classic_path, NetCDFFormat::Classic)
1222 .expect("Operation failed");
1223 let netcdf4 = NetCDFFile::create_with_format(netcdf4_path, NetCDFFormat::NetCDF4)
1224 .expect("Operation failed");
1225
1226 assert_eq!(classic.format(), NetCDFFormat::Classic);
1227 assert_eq!(netcdf4.format(), NetCDFFormat::NetCDF4);
1228
1229 assert!(!classic.has_hdf5_backend());
1230 assert!(netcdf4.has_hdf5_backend());
1231
1232 drop(classic);
1233 drop(netcdf4);
1234 let _ = std::fs::remove_file(classic_file);
1235 let _ = std::fs::remove_file(netcdf4_file);
1236 }
1237
1238 #[test]
1239 fn test_netcdf4_write_array() {
1240 use scirs2_core::ndarray::array;
1241
1242 let temp_dir = std::env::temp_dir();
1243 let test_file = temp_dir.join(format!("test_netcdf4_array_{}.nc", std::process::id()));
1244 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1245
1246 let mut file = NetCDFFile::create_with_format(test_path, NetCDFFormat::NetCDF4)
1247 .expect("Operation failed");
1248
1249 let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1250 let result = file.write_array("test_data", &data, &["x", "y"]);
1251 assert!(result.is_ok());
1252
1253 assert!(file.dimensions().contains_key("x"));
1255 assert!(file.dimensions().contains_key("y"));
1256 assert_eq!(file.dimensions()["x"], Some(2));
1257 assert_eq!(file.dimensions()["y"], Some(3));
1258
1259 assert!(file.variables().contains(&"test_data".to_string()));
1261
1262 drop(file);
1263 let _ = std::fs::remove_file(test_file);
1264 }
1265
1266 #[test]
1267 fn test_netcdf4_convenience_functions() {
1268 use scirs2_core::ndarray::array;
1269 use std::collections::HashMap;
1270
1271 let temp_dir = std::env::temp_dir();
1272 let test_file = temp_dir.join(format!("test_convenience_{}.nc", std::process::id()));
1273 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1274
1275 let mut datasets = HashMap::new();
1276 datasets.insert(
1277 "temperature".to_string(),
1278 (
1279 array![[20.0, 21.0], [22.0, 23.0]].into_dyn(),
1280 vec!["time".to_string(), "location".to_string()],
1281 ),
1282 );
1283
1284 let mut global_attrs = HashMap::new();
1285 global_attrs.insert("title".to_string(), "Test Data".to_string());
1286
1287 let result = create_netcdf4_with_data(test_path, datasets, global_attrs);
1288 assert!(result.is_ok());
1289
1290 let _ = std::fs::remove_file(test_file);
1291 }
1292
1293 #[test]
1294 fn test_classic_netcdf_write_array_error() {
1295 use scirs2_core::ndarray::array;
1296
1297 let temp_dir = std::env::temp_dir();
1298 let test_file = temp_dir.join(format!("test_classic_error_{}.nc", std::process::id()));
1299 let test_path = test_file.to_str().expect("path should be valid UTF-8");
1300
1301 let mut file = NetCDFFile::create_with_format(test_path, NetCDFFormat::Classic)
1302 .expect("Operation failed");
1303
1304 let data = array![[1.0, 2.0], [3.0, 4.0]];
1305 let result = file.write_array("test_data", &data, &["x", "y"]);
1306
1307 assert!(result.is_err());
1309 assert!(result
1310 .unwrap_err()
1311 .to_string()
1312 .contains("only supported for NetCDF4"));
1313
1314 drop(file);
1315 let _ = std::fs::remove_file(test_file);
1316 }
1317}