Skip to main content

netcdf_reader/
masked.rs

1//! _FillValue / missing_value masking for NetCDF variables.
2//!
3//! Replaces fill/missing values with NaN according to CF conventions.
4//! Checks `_FillValue`, `missing_value`, `valid_min`, `valid_max`, and `valid_range`.
5
6use ndarray::ArrayD;
7
8use crate::types::NcVariable;
9
10/// Parameters for masking invalid data.
11#[derive(Debug, Clone)]
12pub struct MaskParams {
13    pub fill_value: Option<f64>,
14    pub missing_value: Option<f64>,
15    pub valid_min: Option<f64>,
16    pub valid_max: Option<f64>,
17}
18
19impl MaskParams {
20    /// Extract masking parameters from a variable's attributes.
21    ///
22    /// Returns `None` if no masking attributes are present.
23    pub fn from_variable(var: &NcVariable) -> Option<Self> {
24        let fill = var.attribute("_FillValue").and_then(|a| a.value.as_f64());
25        let missing = var
26            .attribute("missing_value")
27            .and_then(|a| a.value.as_f64());
28
29        // valid_range takes precedence over valid_min/valid_max individually
30        let (vmin, vmax) = if let Some(range) = var
31            .attribute("valid_range")
32            .and_then(|a| a.value.as_f64_vec())
33        {
34            if range.len() >= 2 {
35                (Some(range[0]), Some(range[1]))
36            } else {
37                (None, None)
38            }
39        } else {
40            let vmin = var.attribute("valid_min").and_then(|a| a.value.as_f64());
41            let vmax = var.attribute("valid_max").and_then(|a| a.value.as_f64());
42            (vmin, vmax)
43        };
44
45        if fill.is_none() && missing.is_none() && vmin.is_none() && vmax.is_none() {
46            return None;
47        }
48
49        Some(MaskParams {
50            fill_value: fill,
51            missing_value: missing,
52            valid_min: vmin,
53            valid_max: vmax,
54        })
55    }
56
57    /// Replace fill/missing values with NaN and mask values outside valid range.
58    ///
59    /// Uses bit-exact comparison for fill/missing values to correctly handle
60    /// NaN fill values (since `NaN != NaN` with normal `==`).
61    pub fn apply(&self, data: &mut ArrayD<f64>) {
62        data.mapv_inplace(|v| {
63            if let Some(fill) = self.fill_value {
64                if v.to_bits() == fill.to_bits() {
65                    return f64::NAN;
66                }
67            }
68            if let Some(miss) = self.missing_value {
69                if v.to_bits() == miss.to_bits() {
70                    return f64::NAN;
71                }
72            }
73            if let Some(vmin) = self.valid_min {
74                if v < vmin {
75                    return f64::NAN;
76                }
77            }
78            if let Some(vmax) = self.valid_max {
79                if v > vmax {
80                    return f64::NAN;
81                }
82            }
83            v
84        });
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::types::{NcAttrValue, NcAttribute, NcType, NcVariable};
92    use ndarray::arr1;
93
94    fn make_var(attrs: Vec<NcAttribute>) -> NcVariable {
95        NcVariable {
96            name: "test".into(),
97            dimensions: vec![],
98            dtype: NcType::Float,
99            attributes: attrs,
100            data_offset: 0,
101            _data_size: 0,
102            is_record_var: false,
103            record_size: 0,
104        }
105    }
106
107    #[test]
108    fn test_no_mask_attrs() {
109        let var = make_var(vec![]);
110        assert!(MaskParams::from_variable(&var).is_none());
111    }
112
113    #[test]
114    fn test_fill_value() {
115        let var = make_var(vec![NcAttribute {
116            name: "_FillValue".into(),
117            value: NcAttrValue::Floats(vec![-9999.0]),
118        }]);
119        let params = MaskParams::from_variable(&var).unwrap();
120        let mut data = arr1(&[1.0, -9999.0, 3.0]).into_dyn();
121        params.apply(&mut data);
122        assert_eq!(data[[0]], 1.0);
123        assert!(data[[1]].is_nan());
124        assert_eq!(data[[2]], 3.0);
125    }
126
127    #[test]
128    fn test_missing_value() {
129        let var = make_var(vec![NcAttribute {
130            name: "missing_value".into(),
131            value: NcAttrValue::Doubles(vec![-999.0]),
132        }]);
133        let params = MaskParams::from_variable(&var).unwrap();
134        let mut data = arr1(&[-999.0, 5.0]).into_dyn();
135        params.apply(&mut data);
136        assert!(data[[0]].is_nan());
137        assert_eq!(data[[1]], 5.0);
138    }
139
140    #[test]
141    fn test_valid_range() {
142        let var = make_var(vec![NcAttribute {
143            name: "valid_range".into(),
144            value: NcAttrValue::Doubles(vec![0.0, 100.0]),
145        }]);
146        let params = MaskParams::from_variable(&var).unwrap();
147        let mut data = arr1(&[-5.0, 50.0, 150.0]).into_dyn();
148        params.apply(&mut data);
149        assert!(data[[0]].is_nan());
150        assert_eq!(data[[1]], 50.0);
151        assert!(data[[2]].is_nan());
152    }
153
154    #[test]
155    fn test_valid_min_max() {
156        let var = make_var(vec![
157            NcAttribute {
158                name: "valid_min".into(),
159                value: NcAttrValue::Doubles(vec![0.0]),
160            },
161            NcAttribute {
162                name: "valid_max".into(),
163                value: NcAttrValue::Doubles(vec![50.0]),
164            },
165        ]);
166        let params = MaskParams::from_variable(&var).unwrap();
167        let mut data = arr1(&[-1.0, 25.0, 51.0]).into_dyn();
168        params.apply(&mut data);
169        assert!(data[[0]].is_nan());
170        assert_eq!(data[[1]], 25.0);
171        assert!(data[[2]].is_nan());
172    }
173}