nexrad_process/derived/
srvel.rs1use crate::result::{Error, Result};
2use crate::SweepProcessor;
3use nexrad_model::data::{GateStatus, SweepField};
4
5pub struct StormRelativeVelocity {
25 storm_direction_degrees: f32,
27 storm_speed_mps: f32,
29}
30
31impl StormRelativeVelocity {
32 pub fn new(storm_direction_degrees: f32, storm_speed_mps: f32) -> Result<Self> {
44 if storm_speed_mps < 0.0 {
45 return Err(Error::InvalidParameter(
46 "storm speed must be non-negative".to_string(),
47 ));
48 }
49 Ok(Self {
50 storm_direction_degrees,
51 storm_speed_mps,
52 })
53 }
54}
55
56impl SweepProcessor for StormRelativeVelocity {
57 fn name(&self) -> &str {
58 "StormRelativeVelocity"
59 }
60
61 fn process(&self, input: &SweepField) -> Result<SweepField> {
62 let mut output = input.clone();
63
64 let storm_to_rad = (self.storm_direction_degrees + 180.0).to_radians();
67
68 let storm_u = self.storm_speed_mps * storm_to_rad.sin();
70 let storm_v = self.storm_speed_mps * storm_to_rad.cos();
71
72 for az_idx in 0..input.azimuth_count() {
73 let azimuth_rad = input.azimuths()[az_idx].to_radians();
74
75 let radial_u = azimuth_rad.sin();
77 let radial_v = azimuth_rad.cos();
78
79 let storm_radial_component = storm_u * radial_u + storm_v * radial_v;
81
82 for gate_idx in 0..input.gate_count() {
83 let (val, status) = input.get(az_idx, gate_idx);
84 if status != GateStatus::Valid {
85 continue;
86 }
87
88 let sr_velocity = val - storm_radial_component;
90 output.set(az_idx, gate_idx, sr_velocity, GateStatus::Valid);
91 }
92 }
93
94 Ok(output)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn make_velocity_field() -> SweepField {
103 let azimuths = vec![0.0, 90.0, 180.0, 270.0];
104 let gate_count = 5;
105
106 let mut field =
107 SweepField::new_empty("Velocity", "m/s", 0.5, azimuths, 1.0, 2.0, 0.25, gate_count);
108
109 for az in 0..4 {
111 for gate in 0..gate_count {
112 field.set(az, gate, 10.0, GateStatus::Valid);
113 }
114 }
115
116 field
117 }
118
119 #[test]
120 fn test_srv_zero_storm_motion() {
121 let field = make_velocity_field();
122 let srv = StormRelativeVelocity::new(0.0, 0.0).unwrap();
123 let result = srv.process(&field).unwrap();
124
125 for az in 0..4 {
127 for gate in 0..5 {
128 let (val, _) = result.get(az, gate);
129 assert!((val - 10.0).abs() < 0.01);
130 }
131 }
132 }
133
134 #[test]
135 fn test_srv_northward_storm() {
136 let field = make_velocity_field();
137 let srv = StormRelativeVelocity::new(180.0, 10.0).unwrap();
139 let result = srv.process(&field).unwrap();
140
141 let (val, _) = result.get(0, 2);
145 assert!(val.abs() < 0.1, "Expected ~0 at north azimuth, got {}", val);
146
147 let (val, _) = result.get(2, 2);
151 assert!(
152 (val - 20.0).abs() < 0.1,
153 "Expected ~20 at south azimuth, got {}",
154 val
155 );
156
157 let (val, _) = result.get(1, 2);
161 assert!(
162 (val - 10.0).abs() < 0.1,
163 "Expected ~10 at east azimuth, got {}",
164 val
165 );
166 }
167
168 #[test]
169 fn test_srv_preserves_nodata() {
170 let mut field = make_velocity_field();
171 field.set(1, 2, 0.0, GateStatus::NoData);
172
173 let srv = StormRelativeVelocity::new(180.0, 10.0).unwrap();
174 let result = srv.process(&field).unwrap();
175
176 let (_, status) = result.get(1, 2);
177 assert_eq!(status, GateStatus::NoData);
178 }
179
180 #[test]
181 fn test_srv_negative_speed_error() {
182 assert!(StormRelativeVelocity::new(0.0, -5.0).is_err());
183 }
184}