Skip to main content

nexrad_process/derived/
composite.rs

1use crate::result::{Error, Result};
2use crate::ScanDerivedProduct;
3use nexrad_model::data::{CartesianField, GateStatus, Scan, SweepField};
4use nexrad_model::geo::{GeoExtent, GeoPoint, RadarCoordinateSystem};
5
6/// Composite reflectivity (CREF) — the maximum reflectivity at each geographic
7/// point across all elevation tilts.
8///
9/// This is one of the most commonly used derived products in operational
10/// meteorology. It provides a plan view of the strongest echoes in the scan
11/// regardless of altitude, making it useful for identifying convective storms.
12///
13/// # Algorithm
14///
15/// For each cell in the output geographic grid:
16/// 1. Convert the cell center to a polar coordinate for each elevation tilt
17/// 2. Sample the reflectivity value at that polar coordinate from each sweep field
18/// 3. Take the maximum valid value across all tilts
19///
20/// # Example
21///
22/// ```ignore
23/// use nexrad_process::derived::CompositeReflectivity;
24/// use nexrad_process::ScanDerivedProduct;
25///
26/// let cref = CompositeReflectivity;
27/// let field = cref.compute(&scan, &ref_fields, &coord_sys, &extent, (800, 800))?;
28/// ```
29pub struct CompositeReflectivity;
30
31impl ScanDerivedProduct for CompositeReflectivity {
32    fn name(&self) -> &str {
33        "CompositeReflectivity"
34    }
35
36    fn compute(
37        &self,
38        _scan: &Scan,
39        fields: &[SweepField],
40        coord_system: &RadarCoordinateSystem,
41        output_extent: &GeoExtent,
42        output_resolution: (usize, usize),
43    ) -> Result<CartesianField> {
44        if fields.is_empty() {
45            return Err(Error::MissingData("no sweep fields provided".to_string()));
46        }
47
48        let (width, height) = output_resolution;
49        if width == 0 || height == 0 {
50            return Err(Error::InvalidParameter(
51                "output resolution must be > 0".to_string(),
52            ));
53        }
54
55        let unit = fields[0].unit().to_string();
56        let mut output = CartesianField::new(
57            "Composite Reflectivity",
58            &unit,
59            *output_extent,
60            width,
61            height,
62        );
63
64        let lat_range = output_extent.max.latitude - output_extent.min.latitude;
65        let lon_range = output_extent.max.longitude - output_extent.min.longitude;
66
67        for row in 0..height {
68            // Row 0 = north edge (max latitude)
69            let lat = output_extent.max.latitude - (row as f64 + 0.5) / height as f64 * lat_range;
70
71            for col in 0..width {
72                let lon =
73                    output_extent.min.longitude + (col as f64 + 0.5) / width as f64 * lon_range;
74
75                let geo_point = GeoPoint {
76                    latitude: lat,
77                    longitude: lon,
78                };
79
80                let mut max_value = f32::MIN;
81                let mut found_valid = false;
82
83                for field in fields {
84                    let polar = coord_system.geo_to_polar(geo_point, field.elevation_degrees());
85
86                    if let Some((val, status)) =
87                        field.value_at_polar(polar.azimuth_degrees, polar.range_km)
88                    {
89                        if status == GateStatus::Valid && val > max_value {
90                            max_value = val;
91                            found_valid = true;
92                        }
93                    }
94                }
95
96                if found_valid {
97                    output.set(row, col, max_value, GateStatus::Valid);
98                }
99            }
100        }
101
102        Ok(output)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use nexrad_model::data::{PulseWidth, Scan, SweepField, VolumeCoveragePattern};
110    use nexrad_model::geo::RadarCoordinateSystem;
111    use nexrad_model::meta::Site;
112
113    fn test_site() -> Site {
114        Site::new(*b"KTLX", 35.3331, -97.2778, 370, 10)
115    }
116
117    fn test_scan() -> Scan {
118        let vcp = VolumeCoveragePattern::new(
119            215,
120            1,
121            0.5,
122            PulseWidth::Short,
123            false,
124            0,
125            false,
126            0,
127            false,
128            false,
129            0,
130            false,
131            false,
132            vec![],
133        );
134        Scan::new(vcp, vec![])
135    }
136
137    fn make_uniform_field(elevation: f32, value: f32) -> SweepField {
138        let mut azimuths = Vec::new();
139        for i in 0..360 {
140            azimuths.push(i as f32);
141        }
142
143        let gate_count = 100;
144        let mut field = SweepField::new_empty(
145            "Reflectivity",
146            "dBZ",
147            elevation,
148            azimuths,
149            1.0,
150            2.0,
151            0.25,
152            gate_count,
153        );
154
155        for az in 0..360 {
156            for gate in 0..gate_count {
157                field.set(az, gate, value, GateStatus::Valid);
158            }
159        }
160
161        field
162    }
163
164    #[test]
165    fn test_cref_single_tilt() {
166        let coord_sys = RadarCoordinateSystem::new(&test_site());
167        let extent = coord_sys.sweep_extent(25.0);
168        let scan = test_scan();
169
170        let fields = vec![make_uniform_field(0.5, 30.0)];
171
172        let result = CompositeReflectivity
173            .compute(&scan, &fields, &coord_sys, &extent, (10, 10))
174            .unwrap();
175
176        // Center pixel should have data
177        let (val, status) = result.get(5, 5);
178        assert_eq!(status, GateStatus::Valid);
179        assert_eq!(val, 30.0);
180    }
181
182    #[test]
183    fn test_cref_takes_max() {
184        let coord_sys = RadarCoordinateSystem::new(&test_site());
185        let extent = coord_sys.sweep_extent(25.0);
186        let scan = test_scan();
187
188        let fields = vec![
189            make_uniform_field(0.5, 20.0),
190            make_uniform_field(1.5, 40.0),
191            make_uniform_field(3.5, 25.0),
192        ];
193
194        let result = CompositeReflectivity
195            .compute(&scan, &fields, &coord_sys, &extent, (10, 10))
196            .unwrap();
197
198        // Center pixel should have the max value (40.0)
199        let (val, status) = result.get(5, 5);
200        assert_eq!(status, GateStatus::Valid);
201        assert_eq!(val, 40.0);
202    }
203
204    #[test]
205    fn test_cref_empty_fields_error() {
206        let coord_sys = RadarCoordinateSystem::new(&test_site());
207        let extent = coord_sys.sweep_extent(25.0);
208        let scan = test_scan();
209
210        let result = CompositeReflectivity.compute(&scan, &[], &coord_sys, &extent, (10, 10));
211        assert!(result.is_err());
212    }
213}