Skip to main content

nexrad_process/filter/
threshold.rs

1use crate::result::Result;
2use crate::SweepProcessor;
3use nexrad_model::data::{GateStatus, SweepField};
4
5/// Masks gates whose values fall outside a specified range.
6///
7/// Gates with values below `min` or above `max` are set to [`GateStatus::NoData`].
8/// Either bound may be `None` to leave that side unbounded.
9///
10/// # Example
11///
12/// ```ignore
13/// use nexrad_process::filter::ThresholdFilter;
14///
15/// // Remove all reflectivity below 5 dBZ
16/// let filter = ThresholdFilter { min: Some(5.0), max: None };
17/// let filtered = filter.process(&field)?;
18/// ```
19pub struct ThresholdFilter {
20    /// Minimum acceptable value. Gates below this are masked.
21    pub min: Option<f32>,
22    /// Maximum acceptable value. Gates above this are masked.
23    pub max: Option<f32>,
24}
25
26impl SweepProcessor for ThresholdFilter {
27    fn name(&self) -> &str {
28        "ThresholdFilter"
29    }
30
31    fn process(&self, input: &SweepField) -> Result<SweepField> {
32        let mut output = input.clone();
33
34        for az_idx in 0..output.azimuth_count() {
35            for gate_idx in 0..output.gate_count() {
36                let (val, status) = output.get(az_idx, gate_idx);
37
38                if status == GateStatus::Valid {
39                    let below_min = self.min.is_some_and(|m| val < m);
40                    let above_max = self.max.is_some_and(|m| val > m);
41
42                    if below_min || above_max {
43                        output.set(az_idx, gate_idx, 0.0, GateStatus::NoData);
44                    }
45                }
46            }
47        }
48
49        Ok(output)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::SweepProcessor;
57
58    fn make_test_field() -> SweepField {
59        let mut field =
60            SweepField::new_empty("Test", "dBZ", 0.5, vec![0.0, 1.0, 2.0], 1.0, 2.0, 0.25, 5);
61
62        // Set some test values
63        for az in 0..3 {
64            for gate in 0..5 {
65                let value = (az * 5 + gate) as f32 * 5.0; // 0, 5, 10, ..., 70
66                field.set(az, gate, value, GateStatus::Valid);
67            }
68        }
69
70        field
71    }
72
73    #[test]
74    fn test_threshold_min_only() {
75        let field = make_test_field();
76        let filter = ThresholdFilter {
77            min: Some(20.0),
78            max: None,
79        };
80
81        let result = filter.process(&field).unwrap();
82
83        // Values below 20 should be masked
84        for az in 0..3 {
85            for gate in 0..5 {
86                let original_value = (az * 5 + gate) as f32 * 5.0;
87                let (val, status) = result.get(az, gate);
88                if original_value < 20.0 {
89                    assert_eq!(status, GateStatus::NoData);
90                    assert_eq!(val, 0.0);
91                } else {
92                    assert_eq!(status, GateStatus::Valid);
93                    assert_eq!(val, original_value);
94                }
95            }
96        }
97    }
98
99    #[test]
100    fn test_threshold_max_only() {
101        let field = make_test_field();
102        let filter = ThresholdFilter {
103            min: None,
104            max: Some(40.0),
105        };
106
107        let result = filter.process(&field).unwrap();
108
109        for az in 0..3 {
110            for gate in 0..5 {
111                let original_value = (az * 5 + gate) as f32 * 5.0;
112                let (_, status) = result.get(az, gate);
113                if original_value > 40.0 {
114                    assert_eq!(status, GateStatus::NoData);
115                } else {
116                    assert_eq!(status, GateStatus::Valid);
117                }
118            }
119        }
120    }
121
122    #[test]
123    fn test_threshold_both_bounds() {
124        let field = make_test_field();
125        let filter = ThresholdFilter {
126            min: Some(15.0),
127            max: Some(50.0),
128        };
129
130        let result = filter.process(&field).unwrap();
131
132        for az in 0..3 {
133            for gate in 0..5 {
134                let original_value = (az * 5 + gate) as f32 * 5.0;
135                let (_, status) = result.get(az, gate);
136                if original_value < 15.0 || original_value > 50.0 {
137                    assert_eq!(status, GateStatus::NoData);
138                } else {
139                    assert_eq!(status, GateStatus::Valid);
140                }
141            }
142        }
143    }
144
145    #[test]
146    fn test_threshold_preserves_nodata() {
147        let mut field = make_test_field();
148        // Mark some gates as already NoData
149        field.set(1, 2, 0.0, GateStatus::NoData);
150
151        let filter = ThresholdFilter {
152            min: Some(0.0),
153            max: None,
154        };
155
156        let result = filter.process(&field).unwrap();
157
158        // The NoData gate should remain NoData
159        let (_, status) = result.get(1, 2);
160        assert_eq!(status, GateStatus::NoData);
161    }
162
163    #[test]
164    fn test_threshold_no_bounds() {
165        let field = make_test_field();
166        let filter = ThresholdFilter {
167            min: None,
168            max: None,
169        };
170
171        let result = filter.process(&field).unwrap();
172
173        // All valid values should remain valid
174        for az in 0..3 {
175            for gate in 0..5 {
176                let (orig_val, _) = field.get(az, gate);
177                let (result_val, result_status) = result.get(az, gate);
178                assert_eq!(result_val, orig_val);
179                assert_eq!(result_status, GateStatus::Valid);
180            }
181        }
182    }
183}