1pub mod classic;
22pub mod error;
23pub mod masked;
24pub mod types;
25pub mod unpack;
26
27#[cfg(feature = "netcdf4")]
28pub mod nc4;
29
30#[cfg(feature = "cf")]
31pub mod cf;
32
33pub use error::{Error, Result};
34pub use types::*;
35
36use std::fs::File;
37use std::path::Path;
38
39use memmap2::Mmap;
40use ndarray::ArrayD;
41#[cfg(feature = "rayon")]
42use rayon::ThreadPool;
43
44#[cfg(feature = "netcdf4")]
50pub trait NcReadable: classic::data::NcReadType + hdf5_reader::H5Type {}
51#[cfg(feature = "netcdf4")]
52impl<T: classic::data::NcReadType + hdf5_reader::H5Type> NcReadable for T {}
53
54#[cfg(not(feature = "netcdf4"))]
55pub trait NcReadable: classic::data::NcReadType {}
56#[cfg(not(feature = "netcdf4"))]
57impl<T: classic::data::NcReadType> NcReadable for T {}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum NcFormat {
62 Classic,
64 Offset64,
66 Cdf5,
68 Nc4,
70 Nc4Classic,
72}
73
74pub struct NcFile {
76 format: NcFormat,
77 inner: NcFileInner,
78}
79
80enum NcFileInner {
81 Classic(classic::ClassicFile),
82 #[cfg(feature = "netcdf4")]
83 Nc4(nc4::Nc4File),
84}
85
86const HDF5_MAGIC: [u8; 8] = [0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
88
89fn detect_format(data: &[u8]) -> Result<NcFormat> {
91 if data.len() < 4 {
92 return Err(Error::InvalidMagic);
93 }
94
95 if data[0] == b'C' && data[1] == b'D' && data[2] == b'F' {
97 return match data[3] {
98 1 => Ok(NcFormat::Classic),
99 2 => Ok(NcFormat::Offset64),
100 5 => Ok(NcFormat::Cdf5),
101 v => Err(Error::UnsupportedVersion(v)),
102 };
103 }
104
105 if data.len() >= 8 && data[..8] == HDF5_MAGIC {
107 return Ok(NcFormat::Nc4);
108 }
109
110 Err(Error::InvalidMagic)
111}
112
113impl NcFile {
114 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
118 let path = path.as_ref();
119 let file = File::open(path)?;
120 let mmap = unsafe { Mmap::map(&file)? };
122 let format = detect_format(&mmap)?;
123
124 match format {
125 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
126 let classic = classic::ClassicFile::from_mmap(mmap, format)?;
127 Ok(NcFile {
128 format,
129 inner: NcFileInner::Classic(classic),
130 })
131 }
132 NcFormat::Nc4 | NcFormat::Nc4Classic => {
133 #[cfg(feature = "netcdf4")]
134 {
135 let nc4 = nc4::Nc4File::open(path)?;
136 let actual_format = if nc4.is_classic_model() {
137 NcFormat::Nc4Classic
138 } else {
139 NcFormat::Nc4
140 };
141 Ok(NcFile {
142 format: actual_format,
143 inner: NcFileInner::Nc4(nc4),
144 })
145 }
146 #[cfg(not(feature = "netcdf4"))]
147 {
148 Err(Error::Nc4NotEnabled)
149 }
150 }
151 }
152 }
153
154 pub fn from_bytes(data: &[u8]) -> Result<Self> {
158 let format = detect_format(data)?;
159
160 match format {
161 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
162 let classic = classic::ClassicFile::from_bytes(data, format)?;
163 Ok(NcFile {
164 format,
165 inner: NcFileInner::Classic(classic),
166 })
167 }
168 NcFormat::Nc4 | NcFormat::Nc4Classic => {
169 #[cfg(feature = "netcdf4")]
170 {
171 let nc4 = nc4::Nc4File::from_bytes(data)?;
172 let actual_format = if nc4.is_classic_model() {
173 NcFormat::Nc4Classic
174 } else {
175 NcFormat::Nc4
176 };
177 Ok(NcFile {
178 format: actual_format,
179 inner: NcFileInner::Nc4(nc4),
180 })
181 }
182 #[cfg(not(feature = "netcdf4"))]
183 {
184 Err(Error::Nc4NotEnabled)
185 }
186 }
187 }
188 }
189
190 pub fn format(&self) -> NcFormat {
192 self.format
193 }
194
195 pub fn root_group(&self) -> &NcGroup {
201 match &self.inner {
202 NcFileInner::Classic(c) => c.root_group(),
203 #[cfg(feature = "netcdf4")]
204 NcFileInner::Nc4(n) => n.root_group(),
205 }
206 }
207
208 pub fn dimensions(&self) -> &[NcDimension] {
210 &self.root_group().dimensions
211 }
212
213 pub fn variables(&self) -> &[NcVariable] {
215 &self.root_group().variables
216 }
217
218 pub fn global_attributes(&self) -> &[NcAttribute] {
220 &self.root_group().attributes
221 }
222
223 pub fn group(&self, path: &str) -> Result<&NcGroup> {
225 self.root_group()
226 .group(path)
227 .ok_or_else(|| Error::GroupNotFound(path.to_string()))
228 }
229
230 pub fn variable(&self, name: &str) -> Result<&NcVariable> {
232 self.root_group()
233 .variable(name)
234 .ok_or_else(|| Error::VariableNotFound(name.to_string()))
235 }
236
237 pub fn dimension(&self, name: &str) -> Result<&NcDimension> {
239 self.root_group()
240 .dimension(name)
241 .ok_or_else(|| Error::DimensionNotFound(name.to_string()))
242 }
243
244 pub fn global_attribute(&self, name: &str) -> Result<&NcAttribute> {
246 self.root_group()
247 .attribute(name)
248 .ok_or_else(|| Error::AttributeNotFound(name.to_string()))
249 }
250
251 pub fn read_variable<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
258 match &self.inner {
259 NcFileInner::Classic(c) => c.read_variable::<T>(name),
260 #[cfg(feature = "netcdf4")]
261 NcFileInner::Nc4(n) => Ok(n.read_variable::<T>(name)?),
262 }
263 }
264
265 #[cfg(feature = "rayon")]
269 pub fn read_variable_parallel<T: NcReadable>(&self, name: &str) -> Result<ArrayD<T>> {
270 match &self.inner {
271 NcFileInner::Classic(c) => c.read_variable::<T>(name),
272 #[cfg(feature = "netcdf4")]
273 NcFileInner::Nc4(n) => Ok(n.read_variable_parallel::<T>(name)?),
274 }
275 }
276
277 #[cfg(feature = "rayon")]
281 pub fn read_variable_in_pool<T: NcReadable>(
282 &self,
283 name: &str,
284 pool: &ThreadPool,
285 ) -> Result<ArrayD<T>> {
286 match &self.inner {
287 NcFileInner::Classic(c) => c.read_variable::<T>(name),
288 #[cfg(feature = "netcdf4")]
289 NcFileInner::Nc4(n) => Ok(n.read_variable_in_pool::<T>(name, pool)?),
290 }
291 }
292
293 pub fn as_classic(&self) -> Option<&classic::ClassicFile> {
297 match &self.inner {
298 NcFileInner::Classic(c) => Some(c),
299 #[cfg(feature = "netcdf4")]
300 NcFileInner::Nc4(_) => None,
301 }
302 }
303
304 pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
310 match &self.inner {
311 NcFileInner::Classic(c) => c.read_variable_as_f64(name),
312 #[cfg(feature = "netcdf4")]
313 NcFileInner::Nc4(n) => n.read_variable_as_f64(name),
314 }
315 }
316
317 pub fn read_variable_unpacked(&self, name: &str) -> Result<ArrayD<f64>> {
323 let var = self.variable(name)?;
324 let params = unpack::UnpackParams::from_variable(var);
325 let mut data = self.read_variable_as_f64(name)?;
326 if let Some(p) = params {
327 p.apply(&mut data);
328 }
329 Ok(data)
330 }
331
332 pub fn read_variable_masked(&self, name: &str) -> Result<ArrayD<f64>> {
336 let var = self.variable(name)?;
337 let params = masked::MaskParams::from_variable(var);
338 let mut data = self.read_variable_as_f64(name)?;
339 if let Some(p) = params {
340 p.apply(&mut data);
341 }
342 Ok(data)
343 }
344
345 pub fn read_variable_unpacked_masked(&self, name: &str) -> Result<ArrayD<f64>> {
350 let var = self.variable(name)?;
351 let mask_params = masked::MaskParams::from_variable(var);
352 let unpack_params = unpack::UnpackParams::from_variable(var);
353 let mut data = self.read_variable_as_f64(name)?;
354 if let Some(p) = mask_params {
355 p.apply(&mut data);
356 }
357 if let Some(p) = unpack_params {
358 p.apply(&mut data);
359 }
360 Ok(data)
361 }
362
363 pub fn read_variable_slice<T: NcReadable>(
367 &self,
368 name: &str,
369 selection: &NcSliceInfo,
370 ) -> Result<ArrayD<T>> {
371 match &self.inner {
372 NcFileInner::Classic(c) => c.read_variable_slice::<T>(name, selection),
373 #[cfg(feature = "netcdf4")]
374 NcFileInner::Nc4(n) => Ok(n.read_variable_slice::<T>(name, selection)?),
375 }
376 }
377
378 #[cfg(feature = "rayon")]
383 pub fn read_variable_slice_parallel<T: NcReadable>(
384 &self,
385 name: &str,
386 selection: &NcSliceInfo,
387 ) -> Result<ArrayD<T>> {
388 match &self.inner {
389 NcFileInner::Classic(c) => c.read_variable_slice::<T>(name, selection),
390 #[cfg(feature = "netcdf4")]
391 NcFileInner::Nc4(n) => Ok(n.read_variable_slice_parallel::<T>(name, selection)?),
392 }
393 }
394
395 pub fn read_variable_slice_as_f64(
397 &self,
398 name: &str,
399 selection: &NcSliceInfo,
400 ) -> Result<ArrayD<f64>> {
401 match &self.inner {
402 NcFileInner::Classic(c) => c.read_variable_slice_as_f64(name, selection),
403 #[cfg(feature = "netcdf4")]
404 NcFileInner::Nc4(n) => n.read_variable_slice_as_f64(name, selection),
405 }
406 }
407
408 pub fn read_variable_slice_unpacked(
410 &self,
411 name: &str,
412 selection: &NcSliceInfo,
413 ) -> Result<ArrayD<f64>> {
414 let var = self.variable(name)?;
415 let params = unpack::UnpackParams::from_variable(var);
416 let mut data = self.read_variable_slice_as_f64(name, selection)?;
417 if let Some(p) = params {
418 p.apply(&mut data);
419 }
420 Ok(data)
421 }
422
423 pub fn read_variable_slice_masked(
425 &self,
426 name: &str,
427 selection: &NcSliceInfo,
428 ) -> Result<ArrayD<f64>> {
429 let var = self.variable(name)?;
430 let params = masked::MaskParams::from_variable(var);
431 let mut data = self.read_variable_slice_as_f64(name, selection)?;
432 if let Some(p) = params {
433 p.apply(&mut data);
434 }
435 Ok(data)
436 }
437
438 pub fn read_variable_slice_unpacked_masked(
440 &self,
441 name: &str,
442 selection: &NcSliceInfo,
443 ) -> Result<ArrayD<f64>> {
444 let var = self.variable(name)?;
445 let mask_params = masked::MaskParams::from_variable(var);
446 let unpack_params = unpack::UnpackParams::from_variable(var);
447 let mut data = self.read_variable_slice_as_f64(name, selection)?;
448 if let Some(p) = mask_params {
449 p.apply(&mut data);
450 }
451 if let Some(p) = unpack_params {
452 p.apply(&mut data);
453 }
454 Ok(data)
455 }
456
457 pub fn iter_slices<T: NcReadable>(
465 &self,
466 name: &str,
467 dim: usize,
468 ) -> Result<NcSliceIterator<'_, T>> {
469 let var = self.variable(name)?;
470 let ndim = var.ndim();
471 if dim >= ndim {
472 return Err(Error::InvalidData(format!(
473 "dimension index {} out of range for {}-dimensional variable '{}'",
474 dim, ndim, name
475 )));
476 }
477 let dim_size = var.dimensions[dim].size;
478 Ok(NcSliceIterator {
479 file: self,
480 name: name.to_string(),
481 dim,
482 dim_size,
483 current: 0,
484 ndim,
485 _marker: std::marker::PhantomData,
486 })
487 }
488}
489
490pub struct NcOpenOptions {
492 pub chunk_cache_bytes: usize,
494 pub chunk_cache_slots: usize,
496 #[cfg(feature = "netcdf4")]
498 pub filter_registry: Option<hdf5_reader::FilterRegistry>,
499}
500
501impl Default for NcOpenOptions {
502 fn default() -> Self {
503 NcOpenOptions {
504 chunk_cache_bytes: 64 * 1024 * 1024,
505 chunk_cache_slots: 521,
506 #[cfg(feature = "netcdf4")]
507 filter_registry: None,
508 }
509 }
510}
511
512impl NcFile {
513 pub fn open_with_options(path: impl AsRef<Path>, options: NcOpenOptions) -> Result<Self> {
515 let path = path.as_ref();
516 let file = File::open(path)?;
517 let mmap = unsafe { Mmap::map(&file)? };
518 let format = detect_format(&mmap)?;
519
520 match format {
521 NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
522 let classic = classic::ClassicFile::from_mmap(mmap, format)?;
523 Ok(NcFile {
524 format,
525 inner: NcFileInner::Classic(classic),
526 })
527 }
528 NcFormat::Nc4 | NcFormat::Nc4Classic => {
529 #[cfg(feature = "netcdf4")]
530 {
531 let hdf5_opts = hdf5_reader::OpenOptions {
532 chunk_cache_bytes: options.chunk_cache_bytes,
533 chunk_cache_slots: options.chunk_cache_slots,
534 filter_registry: options.filter_registry,
535 };
536 let hdf5 = hdf5_reader::Hdf5File::open_with_options(path, hdf5_opts)?;
537 let root_group = nc4::groups::build_root_group(&hdf5)?;
538 let nc4 = nc4::Nc4File::from_hdf5(hdf5, root_group);
539 let actual_format = if nc4.is_classic_model() {
540 NcFormat::Nc4Classic
541 } else {
542 NcFormat::Nc4
543 };
544 Ok(NcFile {
545 format: actual_format,
546 inner: NcFileInner::Nc4(nc4),
547 })
548 }
549 #[cfg(not(feature = "netcdf4"))]
550 {
551 let _ = options;
552 Err(Error::Nc4NotEnabled)
553 }
554 }
555 }
556 }
557}
558
559pub struct NcSliceIterator<'f, T: NcReadable> {
561 file: &'f NcFile,
562 name: String,
563 dim: usize,
564 dim_size: u64,
565 current: u64,
566 ndim: usize,
567 _marker: std::marker::PhantomData<T>,
568}
569
570impl<'f, T: NcReadable> Iterator for NcSliceIterator<'f, T> {
571 type Item = Result<ArrayD<T>>;
572
573 fn next(&mut self) -> Option<Self::Item> {
574 if self.current >= self.dim_size {
575 return None;
576 }
577 let mut selections = Vec::with_capacity(self.ndim);
578 for d in 0..self.ndim {
579 if d == self.dim {
580 selections.push(NcSliceInfoElem::Index(self.current));
581 } else {
582 selections.push(NcSliceInfoElem::Slice {
583 start: 0,
584 end: u64::MAX,
585 step: 1,
586 });
587 }
588 }
589 let selection = NcSliceInfo { selections };
590 self.current += 1;
591 Some(self.file.read_variable_slice::<T>(&self.name, &selection))
592 }
593
594 fn size_hint(&self) -> (usize, Option<usize>) {
595 let remaining = (self.dim_size - self.current) as usize;
596 (remaining, Some(remaining))
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_detect_cdf1() {
606 let data = b"CDF\x01rest_of_file";
607 assert_eq!(detect_format(data).unwrap(), NcFormat::Classic);
608 }
609
610 #[test]
611 fn test_detect_cdf2() {
612 let data = b"CDF\x02rest_of_file";
613 assert_eq!(detect_format(data).unwrap(), NcFormat::Offset64);
614 }
615
616 #[test]
617 fn test_detect_cdf5() {
618 let data = b"CDF\x05rest_of_file";
619 assert_eq!(detect_format(data).unwrap(), NcFormat::Cdf5);
620 }
621
622 #[test]
623 fn test_detect_hdf5() {
624 let mut data = vec![0x89, b'H', b'D', b'F', 0x0D, 0x0A, 0x1A, 0x0A];
625 data.extend_from_slice(b"rest_of_file");
626 assert_eq!(detect_format(&data).unwrap(), NcFormat::Nc4);
627 }
628
629 #[test]
630 fn test_detect_invalid_magic() {
631 let data = b"XXXX";
632 assert!(matches!(
633 detect_format(data).unwrap_err(),
634 Error::InvalidMagic
635 ));
636 }
637
638 #[test]
639 fn test_detect_unsupported_version() {
640 let data = b"CDF\x03";
641 assert!(matches!(
642 detect_format(data).unwrap_err(),
643 Error::UnsupportedVersion(3)
644 ));
645 }
646
647 #[test]
648 fn test_detect_too_short() {
649 let data = b"CD";
650 assert!(matches!(
651 detect_format(data).unwrap_err(),
652 Error::InvalidMagic
653 ));
654 }
655
656 #[test]
657 fn test_from_bytes_minimal_cdf1() {
658 let mut data = Vec::new();
660 data.extend_from_slice(b"CDF\x01");
661 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes());
667 data.extend_from_slice(&0u32.to_be_bytes());
668 data.extend_from_slice(&0u32.to_be_bytes());
670 data.extend_from_slice(&0u32.to_be_bytes());
671
672 let file = NcFile::from_bytes(&data).unwrap();
673 assert_eq!(file.format(), NcFormat::Classic);
674 assert!(file.dimensions().is_empty());
675 assert!(file.variables().is_empty());
676 assert!(file.global_attributes().is_empty());
677 }
678
679 #[test]
680 fn test_from_bytes_cdf1_with_data() {
681 let mut data = Vec::new();
683 data.extend_from_slice(b"CDF\x01");
684 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&0x0000_000Au32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes());
691 data.push(b'x');
692 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&3u32.to_be_bytes());
695
696 data.extend_from_slice(&0x0000_000Cu32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&5u32.to_be_bytes());
701 data.extend_from_slice(b"title");
702 data.extend_from_slice(&[0, 0, 0]); data.extend_from_slice(&2u32.to_be_bytes());
705 data.extend_from_slice(&4u32.to_be_bytes());
707 data.extend_from_slice(b"test"); data.extend_from_slice(&0x0000_000Bu32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&4u32.to_be_bytes());
714 data.extend_from_slice(b"vals");
715 data.extend_from_slice(&1u32.to_be_bytes());
717 data.extend_from_slice(&0u32.to_be_bytes());
719 data.extend_from_slice(&0u32.to_be_bytes());
721 data.extend_from_slice(&0u32.to_be_bytes());
722 data.extend_from_slice(&5u32.to_be_bytes());
724 data.extend_from_slice(&12u32.to_be_bytes());
726 let data_offset = data.len() as u32 + 4; data.extend_from_slice(&data_offset.to_be_bytes());
729
730 data.extend_from_slice(&1.5f32.to_be_bytes());
732 data.extend_from_slice(&2.5f32.to_be_bytes());
733 data.extend_from_slice(&3.5f32.to_be_bytes());
734
735 let file = NcFile::from_bytes(&data).unwrap();
736 assert_eq!(file.format(), NcFormat::Classic);
737 assert_eq!(file.dimensions().len(), 1);
738 assert_eq!(file.dimensions()[0].name, "x");
739 assert_eq!(file.dimensions()[0].size, 3);
740
741 assert_eq!(file.global_attributes().len(), 1);
742 assert_eq!(file.global_attributes()[0].name, "title");
743 assert_eq!(
744 file.global_attributes()[0].value.as_string().unwrap(),
745 "test"
746 );
747
748 assert_eq!(file.variables().len(), 1);
749 let var = file.variable("vals").unwrap();
750 assert_eq!(var.dtype(), &NcType::Float);
751 assert_eq!(var.shape(), vec![3]);
752
753 let classic = file.as_classic().unwrap();
755 let arr: ndarray::ArrayD<f32> = classic.read_variable("vals").unwrap();
756 assert_eq!(arr.shape(), &[3]);
757 assert_eq!(arr[[0]], 1.5f32);
758 assert_eq!(arr[[1]], 2.5f32);
759 assert_eq!(arr[[2]], 3.5f32);
760 }
761
762 #[test]
763 fn test_variable_not_found() {
764 let mut data = Vec::new();
765 data.extend_from_slice(b"CDF\x01");
766 data.extend_from_slice(&0u32.to_be_bytes());
767 data.extend_from_slice(&0u32.to_be_bytes());
769 data.extend_from_slice(&0u32.to_be_bytes());
770 data.extend_from_slice(&0u32.to_be_bytes());
771 data.extend_from_slice(&0u32.to_be_bytes());
772 data.extend_from_slice(&0u32.to_be_bytes());
773 data.extend_from_slice(&0u32.to_be_bytes());
774
775 let file = NcFile::from_bytes(&data).unwrap();
776 assert!(matches!(
777 file.variable("nonexistent").unwrap_err(),
778 Error::VariableNotFound(_)
779 ));
780 }
781
782 #[test]
783 fn test_group_not_found() {
784 let mut data = Vec::new();
785 data.extend_from_slice(b"CDF\x01");
786 data.extend_from_slice(&0u32.to_be_bytes());
787 data.extend_from_slice(&0u32.to_be_bytes());
788 data.extend_from_slice(&0u32.to_be_bytes());
789 data.extend_from_slice(&0u32.to_be_bytes());
790 data.extend_from_slice(&0u32.to_be_bytes());
791 data.extend_from_slice(&0u32.to_be_bytes());
792 data.extend_from_slice(&0u32.to_be_bytes());
793
794 let file = NcFile::from_bytes(&data).unwrap();
795 assert!(matches!(
796 file.group("nonexistent").unwrap_err(),
797 Error::GroupNotFound(_)
798 ));
799 }
800}