1use crate::field::{Wind, WindField};
12use crate::{Error, Result};
13
14const EARTH_RADIUS_M: f64 = 6_371_000.0;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Monitor {
20 pub x_m: f64,
22 pub z_m: f64,
24 pub speed_mps: f64,
26 pub direction_from_deg: f64,
29}
30
31impl Monitor {
32 pub fn at(x_m: f64, z_m: f64, speed_mps: f64, direction_from_deg: f64) -> Self {
34 Self {
35 x_m,
36 z_m,
37 speed_mps,
38 direction_from_deg,
39 }
40 }
41
42 pub fn from_geo(
47 lat_deg: f64,
48 lon_deg: f64,
49 speed_mps: f64,
50 dir_compass_from_deg: f64,
51 ref_lat_deg: f64,
52 ref_lon_deg: f64,
53 north_azimuth_deg: f64,
54 ) -> Self {
55 let (x_m, z_m) = local_en_m(lat_deg, lon_deg, ref_lat_deg, ref_lon_deg);
56 Self {
57 x_m,
58 z_m,
59 speed_mps,
60 direction_from_deg: (dir_compass_from_deg - north_azimuth_deg).rem_euclid(360.0),
61 }
62 }
63}
64
65pub fn local_en_m(lat_deg: f64, lon_deg: f64, ref_lat_deg: f64, ref_lon_deg: f64) -> (f64, f64) {
69 let dlat = (lat_deg - ref_lat_deg).to_radians();
70 let dlon = (lon_deg - ref_lon_deg).to_radians();
71 let east = EARTH_RADIUS_M * dlon * ref_lat_deg.to_radians().cos();
72 let north = EARTH_RADIUS_M * dlat;
73 (east, north)
74}
75
76pub fn monitors_from_geo_csv(
81 text: &str,
82 ref_lat_deg: f64,
83 ref_lon_deg: f64,
84 north_azimuth_deg: f64,
85) -> Result<Vec<Monitor>> {
86 let mut monitors = Vec::new();
87 for (number, line) in text.lines().enumerate() {
88 let line = line.trim();
89 if line.is_empty() || line.starts_with('#') {
90 continue;
91 }
92 let fields: Vec<&str> = line.split(',').collect();
93 if fields.len() < 4 {
94 return Err(Error::invalid(
95 "monitor csv",
96 format!(
97 "line {}: expected lat,lon,speed_ms,dir_compass_deg",
98 number + 1
99 ),
100 ));
101 }
102 let num = |token: &str, what: &str| -> Result<f64> {
103 token.trim().parse().map_err(|_| {
104 Error::invalid(
105 "monitor csv",
106 format!("line {}: {what} '{token}' is not numeric", number + 1),
107 )
108 })
109 };
110 let (lat, lon) = (num(fields[0], "lat")?, num(fields[1], "lon")?);
111 let speed = num(fields[2], "speed")?;
112 let dir = num(fields[3], "direction")?;
113 if !lat.is_finite()
114 || lat.abs() > 90.0
115 || !lon.is_finite()
116 || !speed.is_finite()
117 || speed < 0.0
118 || !dir.is_finite()
119 {
120 return Err(Error::invalid(
121 "monitor csv",
122 format!(
123 "line {}: readings must be finite with nonnegative speed",
124 number + 1
125 ),
126 ));
127 }
128 monitors.push(Monitor::from_geo(
129 lat,
130 lon,
131 speed,
132 dir,
133 ref_lat_deg,
134 ref_lon_deg,
135 north_azimuth_deg,
136 ));
137 }
138 Ok(monitors)
139}
140
141#[derive(Debug, Clone)]
145pub struct HeightGrid {
146 pub origin_x: f64,
148 pub origin_y: f64,
150 pub dx: f64,
152 pub dy: f64,
154 pub ncols: usize,
156 pub nrows: usize,
158 pub heights: Vec<f64>,
160}
161
162impl HeightGrid {
163 pub fn new(
165 origin_x: f64,
166 origin_y: f64,
167 dx: f64,
168 dy: f64,
169 ncols: usize,
170 nrows: usize,
171 heights: Vec<f64>,
172 ) -> Result<Self> {
173 if dx <= 0.0 || dy <= 0.0 || !dx.is_finite() || !dy.is_finite() {
174 return Err(Error::invalid("height grid", "spacing must be positive"));
175 }
176 if ncols < 2 || nrows < 2 {
177 return Err(Error::invalid("height grid", "needs at least 2x2 cells"));
178 }
179 if heights.len() != ncols * nrows {
180 return Err(Error::invalid(
181 "height grid",
182 "height count does not match the grid shape",
183 ));
184 }
185 if !heights.iter().all(|h| h.is_finite()) {
186 return Err(Error::invalid("height grid", "heights must be finite"));
187 }
188 Ok(Self {
189 origin_x,
190 origin_y,
191 dx,
192 dy,
193 ncols,
194 nrows,
195 heights,
196 })
197 }
198
199 pub fn height_at(&self, x: f64, z: f64) -> f64 {
202 let fx = ((x - self.origin_x) / self.dx).clamp(0.0, self.ncols as f64 - 1.0);
203 let fy = ((z - self.origin_y) / self.dy).clamp(0.0, self.nrows as f64 - 1.0);
204 let (j, i) = (fx.floor() as usize, fy.floor() as usize);
205 let (rx, ry) = (fx - j as f64, fy - i as f64);
206 let j1 = (j + 1).min(self.ncols - 1);
207 let i1 = (i + 1).min(self.nrows - 1);
208 let f = |i: usize, j: usize| self.heights[i * self.ncols + j];
209 (1.0 - ry) * ((1.0 - rx) * f(i, j) + rx * f(i, j1))
210 + ry * ((1.0 - rx) * f(i1, j) + rx * f(i1, j1))
211 }
212
213 pub fn from_ascii(text: &str) -> Result<Self> {
217 let mut ncols = None;
218 let mut nrows = None;
219 let mut xll = None;
220 let mut yll = None;
221 let mut cellsize = None;
222 let mut heights = Vec::new();
223 for line in text.lines() {
224 let line = line.trim();
225 if line.is_empty() {
226 continue;
227 }
228 let mut parts = line.split_whitespace();
229 match parts.next().unwrap_or("").to_ascii_lowercase().as_str() {
230 "ncols" => ncols = parse_asc_num(parts.next()),
231 "nrows" => nrows = parse_asc_num(parts.next()),
232 "xllcorner" | "xllcenter" => xll = parse_asc_num(parts.next()),
233 "yllcorner" | "yllcenter" => yll = parse_asc_num(parts.next()),
234 "cellsize" => cellsize = parse_asc_num(parts.next()),
235 "nodata_value" => continue,
236 _ => {
237 for token in line.split_whitespace() {
238 if let Ok(v) = token.parse::<f64>() {
239 heights.push(v);
240 }
241 }
242 }
243 }
244 }
245 let ncols = ncols.ok_or_else(|| Error::invalid("ascii dem", "missing ncols"))? as usize;
246 let nrows = nrows.ok_or_else(|| Error::invalid("ascii dem", "missing nrows"))? as usize;
247 let cellsize = cellsize.ok_or_else(|| Error::invalid("ascii dem", "missing cellsize"))?;
248 let (xll, yll) = (
249 xll.ok_or_else(|| Error::invalid("ascii dem", "missing xllcorner"))?,
250 yll.ok_or_else(|| Error::invalid("ascii dem", "missing yllcorner"))?,
251 );
252 if heights.len() != ncols * nrows {
253 return Err(Error::invalid(
254 "ascii dem",
255 "height count does not match header",
256 ));
257 }
258 let mut north_up = Vec::with_capacity(heights.len());
260 for row in heights.chunks_exact(ncols).rev() {
261 north_up.extend_from_slice(row);
262 }
263 Self::new(xll, yll, cellsize, cellsize, ncols, nrows, north_up)
264 }
265}
266
267fn parse_asc_num(token: Option<&str>) -> Option<f64> {
268 token.and_then(|s| s.parse().ok())
269}
270
271#[derive(Debug, Clone)]
273pub struct TerrainFlowField {
274 grid: HeightGrid,
275 levels_agl: Vec<f64>,
277 u: Vec<f64>,
280 v: Vec<f64>,
281}
282
283impl TerrainFlowField {
284 pub fn adjust(
292 grid: HeightGrid,
293 base: &dyn WindField,
294 monitors: &[Monitor],
295 levels_agl: &[f64],
296 obs_length_m: f64,
297 surface_decay_m: f64,
298 terrain_scale_m: f64,
299 ) -> Result<Self> {
300 if levels_agl.is_empty() || levels_agl.iter().any(|h| !h.is_finite() || *h < 0.0) {
301 return Err(Error::invalid(
302 "levels_agl",
303 "must be finite and nonnegative",
304 ));
305 }
306 if obs_length_m <= 0.0 || surface_decay_m <= 0.0 || terrain_scale_m <= 0.0 {
307 return Err(Error::invalid("terrain", "length scales must be positive"));
308 }
309 if levels_agl.is_empty() || levels_agl.iter().any(|h| !h.is_finite() || *h < 0.0) {
310 return Err(Error::invalid(
311 "levels_agl",
312 "must be finite and nonnegative",
313 ));
314 }
315 if obs_length_m <= 0.0 || surface_decay_m <= 0.0 {
316 return Err(Error::invalid("terrain", "length scales must be positive"));
317 }
318 for m in monitors {
319 if !m.x_m.is_finite() || !m.z_m.is_finite() || !m.direction_from_deg.is_finite() {
320 return Err(Error::invalid("monitor", "must be finite"));
321 }
322 if !m.speed_mps.is_finite() || m.speed_mps < 0.0 {
323 return Err(Error::invalid("monitor", "speed must be nonnegative"));
324 }
325 }
326 let (ncols, nrows) = (grid.ncols, grid.nrows);
327 let n = ncols * nrows;
328 let inv_dx2 = 1.0 / (grid.dx * grid.dx);
329 let inv_dy2 = 1.0 / (grid.dy * grid.dy);
330 let diag = -2.0 * (inv_dx2 + inv_dy2);
331 let omega = 1.6;
332 let mut u = Vec::with_capacity(levels_agl.len() * n);
333 let mut v = Vec::with_capacity(levels_agl.len() * n);
334 let mut phi = vec![0.0; n];
335 for &level in levels_agl {
336 let mut u0 = vec![0.0; n];
338 let mut v0 = vec![0.0; n];
339 for i in 0..nrows {
340 for j in 0..ncols {
341 let x = grid.origin_x + j as f64 * grid.dx;
342 let z = grid.origin_y + i as f64 * grid.dy;
343 let height = grid.heights[i * ncols + j] + level;
344 let bg = base.wind(0.0, [x, height, z]);
345 let (mut cx, mut cz, mut wsum) = (0.0, 0.0, 0.0);
346 for m in monitors {
347 let mw = Wind::from_speed_direction(m.speed_mps, m.direction_from_deg);
348 let bg_m = base.wind(0.0, [m.x_m, grid.height_at(m.x_m, m.z_m), m.z_m]);
349 let dx = x - m.x_m;
350 let dz = z - m.z_m;
351 let weight = (-(dx * dx + dz * dz) / (obs_length_m * obs_length_m)).exp()
352 * (-level / surface_decay_m).exp();
353 cx += weight * (mw.x_mps - bg_m.x_mps);
354 cz += weight * (mw.z_mps - bg_m.z_mps);
355 wsum += weight;
356 }
357 if wsum > 0.0 {
358 let w_floor = 0.1;
361 cx /= wsum + w_floor;
362 cz /= wsum + w_floor;
363 }
364 u0[i * ncols + j] = bg.x_mps + cx;
365 v0[i * ncols + j] = bg.z_mps + cz;
366 }
367 }
368 let level_decay = (-level / terrain_scale_m).exp() / terrain_scale_m;
373 let slopes = |u0x: f64, u0z: f64, i: usize, j: usize| -> f64 {
374 let dhdx = (grid.heights[i * ncols + (j + 1).min(ncols - 1)]
375 - grid.heights[i * ncols + j.saturating_sub(1)])
376 / (2.0 * grid.dx);
377 let dhdz = (grid.heights[(i + 1).min(nrows - 1) * ncols + j]
378 - grid.heights[i.saturating_sub(1) * ncols + j])
379 / (2.0 * grid.dy);
380 u0x * dhdx + u0z * dhdz
381 };
382 phi.fill(0.0);
383 for _ in 0..4000 {
384 let mut max_step = 0.0_f64;
385 for i in 1..nrows - 1 {
386 for j in 1..ncols - 1 {
387 let ip = (i + 1).min(nrows - 1);
388 let im = i.saturating_sub(1);
389 let jp = (j + 1).min(ncols - 1);
390 let jm = j.saturating_sub(1);
391 let lap = (phi[i * ncols + jp] + phi[i * ncols + jm]) * inv_dx2
392 + (phi[ip * ncols + j] + phi[im * ncols + j]) * inv_dy2
393 + diag * phi[i * ncols + j];
394 let div = (u0[i * ncols + jp] - u0[i * ncols + jm]) / (2.0 * grid.dx)
395 + (v0[ip * ncols + j] - v0[im * ncols + j]) / (2.0 * grid.dy);
396 let rhs =
397 div - slopes(u0[i * ncols + j], v0[i * ncols + j], i, j) * level_decay;
398 let updated = phi[i * ncols + j] + omega * (rhs - lap) / diag;
399 max_step = max_step.max((updated - phi[i * ncols + j]).abs());
400 phi[i * ncols + j] = updated;
401 }
402 }
403 if max_step < 1e-6 {
404 break;
405 }
406 }
407 for i in 0..nrows {
408 for j in 0..ncols {
409 let edge = i == 0 || i == nrows - 1 || j == 0 || j == ncols - 1;
410 if edge {
411 u.push(u0[i * ncols + j]);
414 v.push(v0[i * ncols + j]);
415 continue;
416 }
417 let jp = (j + 1).min(ncols - 1);
418 let jm = j.saturating_sub(1);
419 let ip = (i + 1).min(nrows - 1);
420 let im = i.saturating_sub(1);
421 let gx = (phi[i * ncols + jp] - phi[i * ncols + jm]) / (2.0 * grid.dx);
422 let gz = (phi[ip * ncols + j] - phi[im * ncols + j]) / (2.0 * grid.dy);
423 u.push(u0[i * ncols + j] - gx);
424 v.push(v0[i * ncols + j] - gz);
425 }
426 }
427 }
428 Ok(Self {
429 grid,
430 levels_agl: levels_agl.to_vec(),
431 u,
432 v,
433 })
434 }
435
436 fn sample_level(&self, level: usize, x: f64, z: f64) -> (f64, f64) {
437 let (ncols, nrows) = (self.grid.ncols, self.grid.nrows);
438 let fx = ((x - self.grid.origin_x) / self.grid.dx).clamp(0.0, ncols as f64 - 1.0);
439 let fy = ((z - self.grid.origin_y) / self.grid.dy).clamp(0.0, nrows as f64 - 1.0);
440 let (j, i) = (fx.floor() as usize, fy.floor() as usize);
441 let (rx, ry) = (fx - j as f64, fy - i as f64);
442 let j1 = (j + 1).min(ncols - 1);
443 let i1 = (i + 1).min(nrows - 1);
444 let base = level * ncols * nrows;
445 let g = |arr: &[f64], i: usize, j: usize| arr[base + i * ncols + j];
446 let sample = |arr: &[f64]| {
447 (1.0 - ry) * ((1.0 - rx) * g(arr, i, j) + rx * g(arr, i, j1))
448 + ry * ((1.0 - rx) * g(arr, i1, j) + rx * g(arr, i1, j1))
449 };
450 (sample(&self.u), sample(&self.v))
451 }
452
453 pub fn horizontal_at(&self, x: f64, y: f64, z: f64) -> (f64, f64) {
455 let agl = y - self.grid.height_at(x, z);
456 let mut level = 0;
457 for (k, &h) in self.levels_agl.iter().enumerate() {
458 if agl <= h {
459 level = k;
460 break;
461 }
462 level = k;
463 }
464 self.sample_level(level, x, z)
465 }
466}
467
468impl WindField for TerrainFlowField {
469 fn wind(&self, _time_s: f64, position_m: [f64; 3]) -> Wind {
470 let (u, v) = self.horizontal_at(position_m[0], position_m[1], position_m[2]);
471 Wind {
472 x_mps: u,
473 y_mps: 0.0,
474 z_mps: v,
475 }
476 }
477}