1use crate::{
23 MapError,
24 sloping::{CellStencil, Corner, FaceWater, Mesh, WeightScratch},
25};
26
27pub const METHOD_ID: &str = "rasmapper-sloping-velocity/0";
28
29pub fn method_record() -> serde_json::Value {
30 serde_json::json!({
31 "method_id": METHOD_ID,
32 "product": "velocity magnitude (ft/s or m/s), RAS Mapper stored-map rule, independent of the WSE render mode",
33 "inputs": "per mesh: HDF face normals with polyline face lengths; per profile: face-normal velocities (Max: summary Maximum Face Velocity) and the cell WSE already supplied",
34 "faces": "normal velocity plus a tangential component from an unweighted least-squares fit of the face and its two ring neighbours in each cell (when a neighbour is hydraulically connected); connected faces take the mean of both sides",
35 "corners": "inverse polyline-length weighted mean of the face vectors over each hydraulically connected arc; zero where every incident face is dry",
36 "pixels": "cell-face sloping donated weights in binary32, native donation history replayed in source-tile order; drawn where rendered depth > 0",
37 "qualified": "native HEC-RAS 7.0.1: BaldEagleCrkMulti2D bridges p05 (Max, one output time), dam-break p07 2D cells (Max, one output time), Spring Creek eBFE AOI (one output time) bit for bit; Davis p02 Max all but 5 of 20,560 pixels (within 2 ulp)",
38 "not_rendered": "storage areas, 1D; derived products (D x V, D x V^2, Froude, shear) are not rendered here"
39 })
40}
41
42#[derive(Clone, Copy, Debug)]
45pub struct FaceNormal {
46 pub normal: [f32; 2],
47 pub length: f32,
48}
49
50#[derive(Clone, Copy, Default)]
52struct Coef {
53 a11: f32,
54 a12: f32,
55 a22: f32,
56}
57
58impl Coef {
59 fn add(&mut self, n: [f32; 2], w: f32) {
60 self.a11 += n[0] * n[0] * w;
61 self.a12 += n[0] * n[1] * w;
62 self.a22 += n[1] * n[1] * w;
63 }
64 fn complete(&mut self) {
66 let inverse = 1.0 / (self.a11 * self.a22 - self.a12 * self.a12);
67 self.a11 *= inverse;
68 self.a12 *= inverse;
69 self.a22 *= inverse;
70 }
71 fn solve(&self, bx: f32, by: f32) -> [f32; 2] {
72 [
73 self.a22 * bx - self.a12 * by,
74 -self.a12 * bx + self.a11 * by,
75 ]
76 }
77}
78
79pub struct Nodes {
81 pub faces: Vec<[[f32; 2]; 2]>,
83 pub corners: Vec<Vec<[f32; 2]>>,
85}
86
87fn arc(corner: &Corner, slot: usize, connected: impl Fn(usize) -> bool) -> (usize, usize) {
89 let mut end = slot;
90 loop {
91 end = corner.ccw[end];
92 if !(connected(corner.faces[end]) && end != slot) {
93 break;
94 }
95 }
96 let mut start = slot;
97 if start != end {
98 while connected(corner.faces[start]) {
99 start = corner.cw[start];
100 }
101 }
102 (start, end)
103}
104
105pub fn reconstruct(
109 mesh: &Mesh,
110 cell_faces: &[Vec<usize>],
111 water: &[FaceWater],
112 normals: &[FaceNormal],
113 velocity: &[f32],
114) -> Result<Nodes, MapError> {
115 let nf = mesh.faces.len();
116 if water.len() != nf
117 || normals.len() != nf
118 || velocity.len() != nf
119 || cell_faces.len() != mesh.cells.len()
120 {
121 return Err(MapError::Invalid("velocity input dimensions".into()));
122 }
123 let connected = |f: usize| water[f].connection.connected();
124 let neighbours = |cell: usize, face: usize| -> Option<(usize, usize)> {
126 let ring = &cell_faces[cell];
127 let i = ring.iter().position(|&f| f == face)?;
128 let n = ring.len();
129 Some((ring[(i + n - 1) % n], ring[(i + 1) % n]))
131 };
132 let side = |cell: usize, f: usize| -> [f32; 2] {
133 let n = normals[f].normal;
134 let u = velocity[f];
135 let Some((cw, ccw)) = neighbours(cell, f) else {
136 return [u * n[0], u * n[1]];
137 };
138 let mut coef = Coef::default();
139 coef.add(normals[cw].normal, 1.0);
140 coef.add(normals[ccw].normal, 1.0);
141 coef.add(n, 1.0);
142 coef.complete();
143 let t = [n[1], -n[0]];
144 let mut along = 0.0_f32;
145 if connected(cw) || connected(ccw) {
146 let mut bx = normals[cw].normal[0] * velocity[cw];
147 let mut by = normals[cw].normal[1] * velocity[cw];
148 bx += normals[ccw].normal[0] * velocity[ccw];
149 by += normals[ccw].normal[1] * velocity[ccw];
150 bx += n[0] * u;
151 by += n[1] * u;
152 let v = coef.solve(bx, by);
153 along = v[0] * t[0] + v[1] * t[1];
154 }
155 [u * n[0] + along * t[0], u * n[1] + along * t[1]]
156 };
157 let faces: Vec<[[f32; 2]; 2]> = mesh
158 .faces
159 .iter()
160 .enumerate()
161 .map(|(f, face)| {
162 let a = side(face.cells[0], f);
163 let b = side(face.cells[1], f);
164 if connected(f) {
165 let m = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
166 [m, m]
167 } else {
168 [a, b]
169 }
170 })
171 .collect();
172 let inverse_length = |f: usize| 1.0_f32 / normals[f].length;
173 let corners = mesh
174 .corners
175 .iter()
176 .enumerate()
177 .map(|(vertex, corner)| {
178 let k = corner.faces.len();
179 if corner
180 .faces
181 .iter()
182 .all(|&f| water[f].values == [crate::sloping::NODATA; 2])
183 {
184 return vec![[0.0, 0.0]; k];
185 }
186 let at_a = |f: usize| mesh.faces[f].vertices[0] == vertex;
188 (0..k)
189 .map(|slot| {
190 let (start, end) = arc(corner, slot, connected);
191 let (mut x, mut y, mut total) = (0.0_f32, 0.0_f32, 0.0_f32);
192 let mut i = start;
193 loop {
194 let f = corner.faces[i];
195 let w = inverse_length(f);
196 let v = if i == start && !at_a(f) {
197 faces[f][1]
198 } else {
199 faces[f][0]
200 };
201 x += v[0] * w;
202 y += v[1] * w;
203 total += w;
204 i = corner.ccw[i];
205 if i == end {
206 break;
207 }
208 }
209 let f = corner.faces[end];
210 if !connected(f) {
211 let w = inverse_length(f);
212 let v = if at_a(f) { faces[f][1] } else { faces[f][0] };
213 x += v[0] * w;
214 y += v[1] * w;
215 total += w;
216 }
217 [x / total, y / total]
218 })
219 .collect()
220 })
221 .collect();
222 Ok(Nodes { faces, corners })
223}
224
225pub fn pixel(
227 stencil: &CellStencil,
228 cell: usize,
229 mesh: &Mesh,
230 nodes: &Nodes,
231 point: [f64; 2],
232 scratch: &mut WeightScratch,
233) -> Result<f32, MapError> {
234 let weights = stencil.donated_weights(point, scratch)?;
235 Ok(paint(stencil, cell, mesh, nodes, &weights))
236}
237
238fn paint(stencil: &CellStencil, cell: usize, mesh: &Mesh, nodes: &Nodes, weights: &[f32]) -> f32 {
239 let n = weights.len() / 2;
240 let (mut x, mut y) = (0.0_f32, 0.0_f32);
241 for (w, [vertex, slot]) in weights[..n].iter().zip(stencil.corner_slots()) {
242 let v = nodes.corners[vertex][slot];
243 x += v[0] * w;
244 y += v[1] * w;
245 }
246 for (w, f) in weights[n..].iter().zip(stencil.face_ids()) {
247 let v = nodes.faces[f][usize::from(mesh.faces[f].cells[0] != cell)];
248 x += v[0] * w;
249 y += v[1] * w;
250 }
251 (f64::from(x * x + y * y)).sqrt() as f32
252}
253
254#[allow(clippy::too_many_arguments)]
259pub(crate) fn replay_cell(
260 stencil: &CellStencil,
261 cell: usize,
262 mesh: &Mesh,
263 nodes: &Nodes,
264 source_transform: [f64; 6],
265 tile: [usize; 2],
266 window: [usize; 4],
267) -> Result<Vec<(usize, f32)>, MapError> {
268 let ring = &stencil.physical_ring;
269 if ring.is_empty() {
270 return Err(MapError::Unsupported(
271 "velocity donation replay needs the cell perimeter".into(),
272 ));
273 }
274 let gt = source_transform;
275 let [tw, th] = tile;
276 let [col, row, width, height] = window;
277 let fold = |axis: usize, f: fn(f64, f64) -> f64, start: f64| {
278 ring.iter().map(|p| p[axis]).fold(start, f)
279 };
280 let (minx, maxx) = (
281 fold(0, f64::min, f64::INFINITY),
282 fold(0, f64::max, f64::NEG_INFINITY),
283 );
284 let (miny, maxy) = (
285 fold(1, f64::min, f64::INFINITY),
286 fold(1, f64::max, f64::NEG_INFINITY),
287 );
288 let c0 = (((minx - gt[0]) / gt[1]).floor() as i64)
289 .max(col as i64)
290 .max(0) as usize;
291 let c1 = (((maxx - gt[0]) / gt[1]).ceil() as i64)
292 .min((col + width) as i64)
293 .max(0) as usize;
294 let r0 = (((maxy - gt[3]) / gt[5]).floor() as i64)
295 .max(row as i64)
296 .max(0) as usize;
297 let r1 = (((miny - gt[3]) / gt[5]).ceil() as i64)
298 .min((row + height) as i64)
299 .max(0) as usize;
300 let mut output = Vec::new();
301 if c0 >= c1 || r0 >= r1 {
302 return Ok(output);
303 }
304 for ty in r0 / th..=(r1 - 1) / th {
305 for tx in c0 / tw..=(c1 - 1) / tw {
306 let tile_gt = [
307 gt[0] + (tx * tw) as f64 * gt[1],
308 gt[1],
309 0.,
310 gt[3] + (ty * th) as f64 * gt[5],
311 0.,
312 gt[5],
313 ];
314 let owned = crate::ownership::burn_ring(
315 ring,
316 terrio::ops::rasterize::Grid {
317 geo_transform: tile_gt,
318 cols: tw,
319 rows: th,
320 crs: Default::default(),
321 },
322 )?;
323 let mut scratch = WeightScratch::new(stencil.corner_slots().count());
324 let mut native_index = 0;
325 for (i, &inside) in owned.iter().enumerate() {
326 if inside == 0 {
327 continue;
328 }
329 if native_index % 256 == 0 {
330 scratch.clear_donation_history();
331 }
332 native_index += 1;
333 let point = [
334 tile_gt[0] + (i % tw) as f64 * gt[1] + 0.5 * gt[1],
335 tile_gt[3] + (i / tw) as f64 * gt[5] + 0.5 * gt[5],
336 ];
337 let weights = stencil.donated_weights_with_history(point, &mut scratch, true)?;
338 let (c, r) = (tx * tw + i % tw, ty * th + i / tw);
339 if (col..col + width).contains(&c) && (row..row + height).contains(&r) {
340 output.push((
341 (r - row) * width + c - col,
342 paint(stencil, cell, mesh, nodes, &weights),
343 ));
344 }
345 }
346 }
347 }
348 Ok(output)
349}
350
351pub(crate) fn interior(
354 mesh: &Mesh,
355 water: &[FaceWater],
356 stencil: &CellStencil,
357 structure_vertices: &[bool],
358) -> bool {
359 let mut vertices: Vec<usize> = stencil.corner_slots().map(|[v, _]| v).collect();
360 let first: Vec<usize> = vertices
361 .iter()
362 .flat_map(|&v| mesh.corners[v].faces.iter().copied())
363 .collect();
364 for &f in &first {
365 vertices.extend(mesh.faces[f].vertices);
366 }
367 if vertices
368 .iter()
369 .any(|&v| structure_vertices.get(v).copied().unwrap_or(false))
370 {
371 return false;
372 }
373 vertices
374 .iter()
375 .flat_map(|&v| mesh.corners[v].faces.iter())
376 .all(|&f| water[f].connection.connected())
377}
378
379pub fn read_normals(path: &std::path::Path, faces: usize) -> Result<Vec<FaceNormal>, MapError> {
382 let raw = read_f32(path, faces * 3)?;
383 let normals: Vec<FaceNormal> = raw
384 .chunks(3)
385 .map(|p| FaceNormal {
386 normal: [p[0], p[1]],
387 length: p[2],
388 })
389 .collect();
390 if normals.iter().any(|n| {
391 !n.normal.iter().all(|v| v.is_finite())
392 || n.length.is_nan()
393 || n.length <= 0.0
394 || !n.length.is_finite()
395 }) {
396 return Err(MapError::Invalid(
397 "face normals must be finite with positive lengths".into(),
398 ));
399 }
400 Ok(normals)
401}
402
403pub fn read_face_velocity(path: &std::path::Path, faces: usize) -> Result<Vec<f32>, MapError> {
405 let values = read_f32(path, faces)?;
406 if values.iter().any(|v| !v.is_finite()) {
407 return Err(MapError::Invalid("face velocities must be finite".into()));
408 }
409 Ok(values)
410}
411
412#[allow(clippy::type_complexity)]
417pub fn render(
418 meshes: &[(&Mesh, &[Option<CellStencil>], &[f32], &[FaceNormal], &[f32])],
419 window: &crate::sloping::RasterWindow<'_>,
420 depth: &[f32],
421 pool: &rayon::ThreadPool,
422) -> Result<Vec<f32>, MapError> {
423 use rayon::prelude::*;
424 let mut nodes = Vec::with_capacity(meshes.len());
425 for &(mesh, stencils, wse, normals, velocity) in meshes {
426 let water = mesh.reconstruct(wse, 0.001, pool)?.faces;
427 let rings: Vec<Vec<usize>> = stencils
428 .iter()
429 .map(|s| s.as_ref().map_or_else(Vec::new, |s| s.face_ids().collect()))
430 .collect();
431 nodes.push(reconstruct(mesh, &rings, &water, normals, velocity)?);
432 }
433 if depth.len() != window.width * window.height {
434 return Err(MapError::Internal("velocity window size mismatch".into()));
435 }
436 let width = window.width;
437 let flagged: Vec<std::sync::atomic::AtomicBool> = (0..window.owner_cells.len())
438 .map(|_| std::sync::atomic::AtomicBool::new(false))
439 .collect();
440 let mut output = vec![f32::NAN; depth.len()];
441 pool.install(|| {
442 output.par_chunks_mut(4096).enumerate().try_for_each_init(
443 || WeightScratch::new(0),
444 |scratch, (chunk, out)| -> Result<(), MapError> {
445 for (j, v) in out.iter_mut().enumerate() {
446 let i = chunk * 4096 + j;
447 let label = window.owner[i] as usize;
448 if label == 0 || depth[i].is_nan() || depth[i] <= 0.0 {
449 continue;
450 }
451 let [m, cell] = window.owner_cells[label - 1];
452 let Some(stencil) = &meshes[m].1[cell] else {
453 continue;
454 };
455 let point = [
456 window.transform[0] + ((i % width) as f64 + 0.5) * window.transform[1],
457 window.transform[3] + ((i / width) as f64 + 0.5) * window.transform[5],
458 ];
459 *v = pixel(stencil, cell, meshes[m].0, &nodes[m], point, scratch)?;
460 if stencil.history_sensitive(scratch) {
461 flagged[label - 1].store(true, std::sync::atomic::Ordering::Relaxed);
462 }
463 }
464 Ok(())
465 },
466 )
467 })?;
468 let gt = window.source_transform;
469 let col = ((window.transform[0] - gt[0]) / gt[1]).round() as usize;
470 let row = ((window.transform[3] - gt[3]) / gt[5]).round() as usize;
471 for (label, flag) in flagged.iter().enumerate() {
472 if !flag.load(std::sync::atomic::Ordering::Relaxed) {
473 continue;
474 }
475 let [m, cell] = window.owner_cells[label];
476 let stencil = meshes[m].1[cell].as_ref().unwrap();
477 for (i, value) in replay_cell(
478 stencil,
479 cell,
480 meshes[m].0,
481 &nodes[m],
482 gt,
483 window.source_tile,
484 [col, row, window.width, window.height],
485 )? {
486 if window.owner[i] as usize == label + 1 && depth[i] > 0.0 {
487 output[i] = value;
488 }
489 }
490 }
491 Ok(output)
492}
493
494pub fn depth_x_velocity(depth: f32, velocity: f32) -> f32 {
498 depth * velocity
499}
500pub fn depth_x_velocity_squared(depth: f32, velocity: f32) -> f32 {
501 depth * velocity * velocity
502}
503pub fn froude(depth: f32, velocity: f32) -> f32 {
504 (f64::from(velocity) / (f64::from(32.174_f32) * f64::from(depth)).sqrt()) as f32
505}
506
507fn read_f32(path: &std::path::Path, count: usize) -> Result<Vec<f32>, MapError> {
508 let raw = std::fs::read(path)?;
509 if raw.len() != count * 4 {
510 return Err(MapError::Invalid(format!("{}: length", path.display())));
511 }
512 Ok(raw
513 .as_chunks::<4>()
514 .0
515 .iter()
516 .map(|b| f32::from_le_bytes(*b))
517 .collect())
518}
519
520#[doc(hidden)]
525pub fn probe(
526 request: &std::path::Path,
527 inputs: &std::path::Path,
528 out: &std::path::Path,
529) -> Result<(), MapError> {
530 use crate::model;
531 let req: crate::sloping_session::Request = model::read_json(request)?;
532 let base = request
533 .parent()
534 .ok_or_else(|| MapError::Invalid("request parent".into()))?;
535 let pool = rayon::ThreadPoolBuilder::new()
536 .build()
537 .map_err(|e| MapError::Internal(e.to_string()))?;
538 let mut meshes = Vec::new();
539 for (m, file) in req.mesh_files.iter().enumerate() {
540 let geometry = crate::sloping_binary::load(&model::resolve(base, file))?;
541 let cell_faces: Vec<Vec<usize>> = geometry.cells.iter().map(|c| c.faces.clone()).collect();
542 let (mesh, stencils) = geometry.prepare()?;
543 let (nc, nf) = (mesh.cells.len(), mesh.faces.len());
544 let raw = read_f32(&inputs.join(format!("normals-{m}.f32")), nf * 3)?;
545 let normals: Vec<FaceNormal> = raw
546 .chunks(3)
547 .map(|p| FaceNormal {
548 normal: [p[0], p[1]],
549 length: p[2],
550 })
551 .collect();
552 let velocity = read_f32(&inputs.join(format!("fv-{m}.f32")), nf)?;
553 let wse = read_f32(&inputs.join(format!("ws-{m}.f32")), nc)?;
554 let wet: Vec<bool> = mesh
555 .cells
556 .iter()
557 .zip(&wse)
558 .map(|(c, &w)| !c.virtual_cell && w > c.minimum + 0.001_f32)
559 .collect();
560 let water = mesh.reconstruct(&wse, 0.001, &pool)?.faces;
561 let nodes = reconstruct(&mesh, &cell_faces, &water, &normals, &velocity)?;
562 let mut structure = vec![false; mesh.corners.len()];
563 let path = inputs.join(format!("structure-vertices-{m}.u32"));
564 if path.exists() {
565 for v in std::fs::read(&path)?.chunks(4) {
566 let v = u32::from_le_bytes(v.try_into().unwrap()) as usize;
567 if let Some(x) = structure.get_mut(v) {
568 *x = true;
569 }
570 }
571 }
572 meshes.push((mesh, stencils, nodes, wet, water, structure));
573 }
574 let [col, row, width, height] = req.window;
575 let gt = req.terrain_transform;
576 let transform = [
577 gt[0] + col as f64 * gt[1],
578 gt[1],
579 0.0,
580 gt[3] + row as f64 * gt[5],
581 0.0,
582 gt[5],
583 ];
584 let grid = terrio::ops::rasterize::Grid {
585 geo_transform: transform,
586 cols: width,
587 rows: height,
588 crs: Default::default(),
589 };
590 let layer = crate::ownership::load(
591 &model::resolve(base, &req.geometry_file),
592 req.owner_cells.len(),
593 false,
594 )?;
595 let owner = crate::ownership::burn(layer, grid)?;
596 let mut output = vec![f32::NAN; width * height];
597 let mut sensitive = vec![0.0_f32; width * height];
598 let mut replay = std::collections::BTreeSet::new();
599 let mut scratch = WeightScratch::new(0);
600 for (i, &label) in owner.iter().enumerate() {
601 if label == 0 {
602 continue;
603 }
604 let [m, cell] = req.owner_cells[label as usize - 1];
605 let (mesh, stencils, nodes, wet, water, structure) = &meshes[m];
606 if !wet[cell] {
607 continue;
608 }
609 let Some(stencil) = &stencils[cell] else {
610 continue;
611 };
612 let point = [
613 transform[0] + ((i % width) as f64 + 0.5) * transform[1],
614 transform[3] + ((i / width) as f64 + 0.5) * transform[5],
615 ];
616 output[i] = pixel(stencil, cell, mesh, nodes, point, &mut scratch)?;
617 if stencil.history_sensitive(&scratch) {
618 replay.insert((m, cell));
619 }
620 sensitive[i] = f32::from(u8::from(stencil.history_sensitive(&scratch)))
621 + 2.0 * f32::from(u8::from(interior(mesh, water, stencil, structure)));
622 }
623 for &(m, cell) in &replay {
624 let (mesh, stencils, nodes, _, _, _) = &meshes[m];
625 let stencil = stencils[cell].as_ref().unwrap();
626 for (i, value) in replay_cell(stencil, cell, mesh, nodes, gt, [256, 256], req.window)? {
627 if owner[i] != 0 && req.owner_cells[owner[i] as usize - 1] == [m, cell] {
628 output[i] = value;
629 }
630 }
631 }
632 crate::horizontal::write_values(&out.with_extension("flag"), &sensitive)?;
633 let labels: Vec<f32> = owner.iter().map(|&l| l as f32).collect();
634 crate::horizontal::write_values(&out.with_extension("owner"), &labels)?;
635 crate::horizontal::write_values(out, &output)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn coefficient_inverse_solves_two_normals() {
644 let mut c = Coef::default();
645 c.add([1.0, 0.0], 1.0);
646 c.add([0.0, 1.0], 1.0);
647 c.complete();
648 assert_eq!(c.solve(3.0, 4.0), [3.0, 4.0]);
649 let mut c = Coef::default();
650 c.add([1.0, 0.0], 0.5);
651 c.add([0.0, 1.0], 2.0);
652 c.complete();
653 assert_eq!(c.solve(1.5, 8.0), [3.0, 4.0]);
654 }
655
656 #[test]
657 fn arc_walks_connected_faces() {
658 let corner = Corner {
660 point: [0.0, 0.0],
661 faces: vec![10, 11, 12, 13],
662 ccw: vec![1, 2, 3, 0],
663 cw: vec![3, 0, 1, 2],
664 };
665 let connected = |f: usize| f == 11;
666 assert_eq!(arc(&corner, 0, connected), (0, 2));
667 assert_eq!(arc(&corner, 1, connected), (0, 2));
668 assert_eq!(arc(&corner, 2, connected), (2, 3));
669 let all = |_f: usize| true;
670 assert_eq!(arc(&corner, 0, all), (0, 0));
671 }
672
673 #[test]
674 fn derived_arithmetic_order() {
675 let (d, v) = (2.5_f32, 1.3_f32);
676 assert_eq!(depth_x_velocity(d, v).to_bits(), (d * v).to_bits());
677 assert_eq!(
678 depth_x_velocity_squared(d, v).to_bits(),
679 ((d * v) * v).to_bits()
680 );
681 assert_eq!(froude(1.0, 0.0), 0.0);
682 }
683}