Skip to main content

netcdf_reader/
unpack.rs

1//! scale_factor / add_offset unpacking for NetCDF variables.
2//!
3//! The CF convention defines:
4//!   `actual_value = stored_value * scale_factor + add_offset`
5//!
6//! Defaults: `scale_factor = 1.0`, `add_offset = 0.0`.
7
8use ndarray::ArrayD;
9
10use crate::types::NcVariable;
11
12/// Parameters for unpacking packed variable data.
13#[derive(Debug, Clone, Copy)]
14pub struct UnpackParams {
15    pub scale_factor: f64,
16    pub add_offset: f64,
17}
18
19impl UnpackParams {
20    /// Extract unpacking parameters from a variable's attributes.
21    ///
22    /// Returns `None` if neither `scale_factor` nor `add_offset` is present
23    /// (i.e., no unpacking needed).
24    pub fn from_variable(var: &NcVariable) -> Option<Self> {
25        let scale = var.attribute("scale_factor").and_then(|a| a.value.as_f64());
26        let offset = var.attribute("add_offset").and_then(|a| a.value.as_f64());
27
28        if scale.is_none() && offset.is_none() {
29            return None;
30        }
31
32        Some(UnpackParams {
33            scale_factor: scale.unwrap_or(1.0),
34            add_offset: offset.unwrap_or(0.0),
35        })
36    }
37
38    /// Apply unpacking: `actual = stored * scale_factor + add_offset`.
39    pub fn apply(&self, data: &mut ArrayD<f64>) {
40        if self.scale_factor == 1.0 && self.add_offset == 0.0 {
41            return;
42        }
43        data.mapv_inplace(|v| v * self.scale_factor + self.add_offset);
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::types::{NcAttrValue, NcAttribute, NcType, NcVariable};
51    use ndarray::arr1;
52
53    fn make_var(attrs: Vec<NcAttribute>) -> NcVariable {
54        NcVariable {
55            name: "test".into(),
56            dimensions: vec![],
57            dtype: NcType::Int,
58            attributes: attrs,
59            data_offset: 0,
60            _data_size: 0,
61            is_record_var: false,
62            record_size: 0,
63        }
64    }
65
66    #[test]
67    fn test_no_unpack_attrs() {
68        let var = make_var(vec![]);
69        assert!(UnpackParams::from_variable(&var).is_none());
70    }
71
72    #[test]
73    fn test_scale_only() {
74        let var = make_var(vec![NcAttribute {
75            name: "scale_factor".into(),
76            value: NcAttrValue::Doubles(vec![0.01]),
77        }]);
78        let params = UnpackParams::from_variable(&var).unwrap();
79        assert_eq!(params.scale_factor, 0.01);
80        assert_eq!(params.add_offset, 0.0);
81    }
82
83    #[test]
84    fn test_offset_only() {
85        let var = make_var(vec![NcAttribute {
86            name: "add_offset".into(),
87            value: NcAttrValue::Doubles(vec![273.15]),
88        }]);
89        let params = UnpackParams::from_variable(&var).unwrap();
90        assert_eq!(params.scale_factor, 1.0);
91        assert_eq!(params.add_offset, 273.15);
92    }
93
94    #[test]
95    fn test_both() {
96        let var = make_var(vec![
97            NcAttribute {
98                name: "scale_factor".into(),
99                value: NcAttrValue::Doubles(vec![0.1]),
100            },
101            NcAttribute {
102                name: "add_offset".into(),
103                value: NcAttrValue::Doubles(vec![10.0]),
104            },
105        ]);
106        let params = UnpackParams::from_variable(&var).unwrap();
107        let mut data = arr1(&[100.0, 200.0, 300.0]).into_dyn();
108        params.apply(&mut data);
109        assert_eq!(data[[0]], 20.0); // 100 * 0.1 + 10
110        assert_eq!(data[[1]], 30.0); // 200 * 0.1 + 10
111        assert_eq!(data[[2]], 40.0); // 300 * 0.1 + 10
112    }
113}