Skip to main content

ras_commander_fast_map/
velocity.rs

1//! Research kernel: RAS Mapper velocity reconstruction (`rasmapper-sloping-velocity/0`).
2//!
3//! **Not wired into any mapping request.** It reproduces the native structure
4//! of RAS Mapper's stored-map velocity so that the remaining differences can be
5//! closed before a product is exposed:
6//!
7//! 1. face classification: the sloping face water surfaces already used by the
8//!    WSE renderer (`Mesh::reconstruct`); "connected" excludes `None`/`Levee`;
9//! 2. face vectors, per cell side: the face's normal velocity plus a tangential
10//!    component from an unweighted least-squares fit of the face and its two
11//!    ring neighbours in that cell (only when a neighbour is connected); a
12//!    connected face takes the mean of its two sides;
13//! 3. corner vectors, per vertex and hydraulically connected arc: the
14//!    inverse-length weighted mean of the arc's face vectors (zero when every
15//!    incident face is dry on both sides);
16//! 4. pixel: the cell-face sloping donated weights applied to the corner and
17//!    face vectors in binary32, magnitude `sqrt(x*x + y*y)`.
18//!
19//! Max velocity uses `Maximum Face Velocity` and maximum WSE as one profile.
20//! Velocity does not depend on the WSE render mode. [`interior`] is the
21//! candidate qualification scope; measurements are in the PR.
22use 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/// Face geometry the native velocity stencils need: HDF `Faces NormalUnitVector
43/// and Length` (unit normal from cell A to cell B, and face length).
44#[derive(Clone, Copy, Debug)]
45pub struct FaceNormal {
46    pub normal: [f32; 2],
47    pub length: f32,
48}
49
50/// 2 x 2 normal-equation accumulator, solved by its scaled inverse (binary32).
51#[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    /// Scaled inverse through one binary32 reciprocal of the determinant.
65    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
79/// Reconstructed node vectors of one profile.
80pub struct Nodes {
81    /// Per face: vector on the cell-A side and on the cell-B side.
82    pub faces: Vec<[[f32; 2]; 2]>,
83    /// Per vertex and incident-face slot: the hydraulically connected arc vector.
84    pub corners: Vec<Vec<[f32; 2]>>,
85}
86
87/// Hydraulically connected arc of `slot` around a corner: (start, end) slots.
88fn 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
105/// Face vectors (C stencil) and corner vectors (inverse-length weighted least
106/// squares over each hydraulically connected arc), from face-normal velocities.
107/// `cell_faces` lists each cell's faces in ring order (as the stencils do).
108pub 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    // Neighbouring faces of `face` within `cell`: (clockwise, counter-clockwise).
125    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        // Rings are counter-clockwise: the next face is the counter-clockwise one.
130        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            // Side of face f that starts (true) or ends at this vertex.
187            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
225/// Velocity magnitude at `point` inside `stencil`'s cell `cell`.
226pub 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/// Native donation history: RAS Mapper computes a cell's donated weights in
255/// source-tile raster order, keeping the clockwise/counter-clockwise donation
256/// scratch between points and clearing it every 256 points. Replays one cell
257/// whose clipped weights expose that state; returns (window index, velocity).
258#[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
351/// Every face incident to the cell's corners, and to those faces' far
352/// vertices, is hydraulically connected (so no perimeter or dry face).
353pub(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
379/// Reads HDF-ordered float32 (x, y, length) face normals; `length` must be the
380/// face polyline length (RAS Mapper's inverse-length weights use it).
381pub 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
403/// Reads one profile's face-normal velocities (float32, one per face, finite).
404pub 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/// Velocity magnitude for a rendered window: drawn where `depth > 0` (NaN
413/// elsewhere), from each mesh's cell WSE and face-normal velocities. Cells whose
414/// clipped weights expose native donation history are replayed in source-tile
415/// order. `meshes[m]` = (mesh, stencils, cell WSE, face normals, face velocity).
416#[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
494/// Derived products from a pixel's binary32 depth and velocity at one output
495/// time. D x V and D x V^2 reproduce native bits given native depth and
496/// velocity; Froude is within one binary32 ulp (native uses unrounded inputs).
497pub 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/// Research probe: renders velocity magnitude for every owned pixel of wet
521/// cells of a staged single-mesh sloping request (NaN elsewhere) and writes it
522/// as little-endian float32 in window order. `inputs` holds HDF-ordered float32
523/// per mesh `m`: `normals-m.f32` (x, y, length), `fv-m.f32` and `ws-m.f32`.
524#[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        // Four faces around a corner; only face 1 is connected.
659        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}