Skip to main content

scirs2_integrate/amr/
level_set.rs

1//! Level-set interface tracking on an AMR quad-tree.
2//!
3//! # Level-Set Method
4//!
5//! The level-set method represents a moving interface Γ as the zero contour of
6//! a smooth signed-distance function φ:
7//!
8//! ```text
9//! Γ(t) = { x : φ(x,t) = 0 }
10//! φ(x,t) > 0  →  x is outside the interface
11//! φ(x,t) < 0  →  x is inside
12//! |∇φ| = 1    →  signed-distance property
13//! ```
14//!
15//! The level-set equation is:
16//! ```text
17//! ∂φ/∂t + v·∇φ = 0
18//! ```
19//!
20//! # Reinitialization
21//!
22//! After advection the signed-distance property degrades.  Reinitialization
23//! solves the Eikonal equation via the Sussman (1994) PDE:
24//! ```text
25//! ∂φ/∂τ + sign(φ₀)(|∇φ| − 1) = 0
26//! ```
27//! using pseudo-time iterations τ on a narrow band around the interface.
28
29use crate::amr::quadtree::{CellData, CellId, Morton2D, QuadTree};
30use std::collections::HashMap;
31
32// ─────────────────────────────────────────────────────────────────────────────
33// Helpers
34// ─────────────────────────────────────────────────────────────────────────────
35
36/// Smoothed sign function (Sussman 1994):
37///
38/// ```text
39/// sign_ε(φ) = φ / √(φ² + ε²)
40/// ```
41#[inline]
42fn sign_smooth(phi: f64, eps: f64) -> f64 {
43    phi / (phi * phi + eps * eps).sqrt()
44}
45
46/// First-order upwind gradient magnitude |∇φ| using Godunov's scheme.
47///
48/// For a scalar φ and upwind differences:
49/// ```text
50/// a⁺ = max(D⁻φ,  0)
51/// a⁻ = min(D⁺φ,  0)
52/// b⁺ = max(D⁻φy, 0)
53/// b⁻ = min(D⁺φy, 0)
54/// |∇φ|² = max(a⁺², a⁻²) + max(b⁺², b⁻²)
55/// ```
56fn godunov_gradient_magnitude(
57    phi_c: f64,
58    phi_xm: f64,
59    phi_xp: f64,
60    phi_ym: f64,
61    phi_yp: f64,
62    dx: f64,
63    dy: f64,
64    sign: f64,
65) -> f64 {
66    let dm_x = (phi_c - phi_xm) / dx;
67    let dp_x = (phi_xp - phi_c) / dx;
68    let dm_y = (phi_c - phi_ym) / dy;
69    let dp_y = (phi_yp - phi_c) / dy;
70
71    // Godunov numerical Hamiltonian
72    let ax = if sign > 0.0 {
73        f64::max(f64::max(dm_x, 0.0).powi(2), f64::min(dp_x, 0.0).powi(2))
74    } else {
75        f64::max(f64::min(dm_x, 0.0).powi(2), f64::max(dp_x, 0.0).powi(2))
76    };
77    let ay = if sign > 0.0 {
78        f64::max(f64::max(dm_y, 0.0).powi(2), f64::min(dp_y, 0.0).powi(2))
79    } else {
80        f64::max(f64::min(dm_y, 0.0).powi(2), f64::max(dp_y, 0.0).powi(2))
81    };
82    (ax + ay).sqrt()
83}
84
85// ─────────────────────────────────────────────────────────────────────────────
86// LevelSet
87// ─────────────────────────────────────────────────────────────────────────────
88
89/// Signed-distance level-set function stored on a `QuadTree`.
90///
91/// Variable index 0 of each leaf cell contains the level-set value φ.  The
92/// tree must have been constructed with `n_vars >= 1`.
93///
94/// The *narrow band* is the set of cells where `|φ| < delta`.  Only narrow-
95/// band cells are updated during reinitialization and advection, which makes
96/// these algorithms O(N_interface) rather than O(N_total).
97pub struct LevelSet {
98    /// The underlying quad-tree (n_vars == 1, storing φ).
99    pub tree: QuadTree,
100    /// Narrow-band half-width in physical units.
101    pub delta: f64,
102}
103
104impl LevelSet {
105    /// Create a new level-set by evaluating `phi0(x, y)` on every leaf of
106    /// an *already-constructed* quad-tree (must have `n_vars >= 1`).
107    ///
108    /// `phi0` should return a **signed distance** (positive outside, negative
109    /// inside) but any smooth function is accepted.
110    pub fn new(tree: QuadTree, phi0: impl Fn(f64, f64) -> f64) -> Self {
111        let mut ls = LevelSet { tree, delta: 3.0 };
112        // Evaluate phi0 on all leaf cells
113        let leaves: Vec<CellId> = ls.tree.leaves();
114        for id in leaves {
115            if let Some(cell) = ls.tree.cells.get(&id) {
116                let (cx, cy) = cell.center(&ls.tree.domain);
117                let phi = phi0(cx, cy);
118                let n_vars = ls.tree.n_vars;
119                let mut vals = vec![0.0f64; n_vars];
120                vals[0] = phi;
121                // Store back
122                if let Some(cell) = ls.tree.cells.get_mut(&id) {
123                    cell.values = vals;
124                }
125            }
126        }
127        // Set delta to 3 * coarsest cell size
128        let coarsest_dx = ls.tree.dx_at_level(0);
129        ls.delta = 3.0 * coarsest_dx;
130        ls
131    }
132
133    /// Override the narrow-band half-width (default: 3 × coarsest cell size).
134    pub fn set_delta(&mut self, delta: f64) {
135        self.delta = delta;
136    }
137
138    /// Return the level-set value φ at a leaf cell.
139    pub fn phi(&self, id: CellId) -> Option<f64> {
140        self.tree.cells.get(&id).map(|c| c.values[0])
141    }
142
143    /// Build a snapshot map `CellId → φ` for all leaf cells.
144    fn phi_snapshot(&self) -> HashMap<CellId, f64> {
145        self.tree
146            .leaves()
147            .into_iter()
148            .filter_map(|id| {
149                let phi = self.tree.cells.get(&id)?.values[0];
150                Some((id, phi))
151            })
152            .collect()
153    }
154
155    /// Look up φ in a snapshot or fall back to the nearest ancestor in the tree.
156    fn get_phi_or_ancestor(snap: &HashMap<CellId, f64>, tree: &QuadTree, id: CellId) -> f64 {
157        if let Some(&phi) = snap.get(&id) {
158            return phi;
159        }
160        // Walk up to find the first ancestor that is in the snapshot
161        let mut cur = id;
162        loop {
163            match cur.parent() {
164                None => return 0.0,
165                Some(p) => {
166                    if let Some(&phi) = snap.get(&p) {
167                        return phi;
168                    }
169                    cur = p;
170                }
171            }
172        }
173    }
174
175    /// Return the φ value of a face-neighbour of `id` in the direction given
176    /// by `(dx, dy) ∈ {±1} × {0}` or `{0} × {±1}`.
177    ///
178    /// If the neighbour does not exist (boundary), the current cell's φ is
179    /// returned (zero-flux Neumann condition).
180    fn neighbour_phi(&self, id: CellId, snap: &HashMap<CellId, f64>, di: i64, dj: i64) -> f64 {
181        let lv = id.level();
182        let size = 1u32.checked_shl(lv as u32).unwrap_or(u32::MAX);
183        let (ix, iy) = id.grid_coords();
184        let ni = ix as i64 + di;
185        let nj = iy as i64 + dj;
186        if ni < 0 || nj < 0 || ni >= size as i64 || nj >= size as i64 {
187            // Boundary: Neumann
188            return snap.get(&id).copied().unwrap_or(0.0);
189        }
190        let nbr_id = CellId::new(lv, Morton2D::encode(ni as u32, nj as u32));
191        Self::get_phi_or_ancestor(snap, &self.tree, nbr_id)
192    }
193
194    /// Sussman reinitialization: solve `∂φ/∂τ + sign(φ₀)(|∇φ|−1) = 0`
195    /// for `n_iters` pseudo-time steps using first-order Godunov upwind.
196    ///
197    /// Only narrow-band cells (`|φ| < delta`) are updated.
198    pub fn reinitialize(&mut self, n_iters: usize) {
199        for _ in 0..n_iters {
200            let snap = self.phi_snapshot();
201            let leaves: Vec<CellId> = self.tree.leaves().into_iter().collect();
202
203            let mut updates: Vec<(CellId, f64)> = Vec::new();
204
205            for &id in &leaves {
206                let phi_c = match snap.get(&id) {
207                    Some(&v) => v,
208                    None => continue,
209                };
210                // Only update narrow-band cells
211                if phi_c.abs() >= self.delta {
212                    continue;
213                }
214
215                let lv = id.level();
216                let dx = self.tree.dx_at_level(lv);
217                let dy = self.tree.dy_at_level(lv);
218                let eps = 0.5 * dx.min(dy);
219
220                let sign0 = sign_smooth(phi_c, eps);
221
222                let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
223                let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
224                let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
225                let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
226
227                let grad_mag = godunov_gradient_magnitude(
228                    phi_c, phi_xm, phi_xp, phi_ym, phi_yp, dx, dy, sign0,
229                );
230
231                // CFL: dt = 0.5 * min(dx, dy)
232                let dt = 0.5 * dx.min(dy);
233                let new_phi = phi_c - dt * sign0 * (grad_mag - 1.0);
234                updates.push((id, new_phi));
235            }
236
237            // Apply updates
238            for (id, new_phi) in updates {
239                if let Some(cell) = self.tree.cells.get_mut(&id) {
240                    cell.values[0] = new_phi;
241                }
242            }
243        }
244    }
245
246    /// First-order upwind advection: `∂φ/∂t + v·∇φ = 0`.
247    ///
248    /// `vx(x, y, t)` and `vy(x, y, t)` are the advecting velocity components.
249    /// Only narrow-band cells are updated.  The time step `dt` must satisfy the
250    /// CFL condition `dt * max(|v|) / dx < 1`.
251    pub fn advect(
252        &mut self,
253        vx: &dyn Fn(f64, f64, f64) -> f64,
254        vy: &dyn Fn(f64, f64, f64) -> f64,
255        t: f64,
256        dt: f64,
257    ) {
258        let snap = self.phi_snapshot();
259        let leaves: Vec<CellId> = self.tree.leaves();
260        let mut updates: Vec<(CellId, f64)> = Vec::new();
261
262        for &id in &leaves {
263            let phi_c = match snap.get(&id) {
264                Some(&v) => v,
265                None => continue,
266            };
267            if phi_c.abs() >= self.delta {
268                continue;
269            }
270
271            let lv = id.level();
272            let dx = self.tree.dx_at_level(lv);
273            let dy = self.tree.dy_at_level(lv);
274
275            let cell = match self.tree.cells.get(&id) {
276                Some(c) => c,
277                None => continue,
278            };
279            let (cx, cy) = cell.center(&self.tree.domain);
280            let u = vx(cx, cy, t);
281            let v_y = vy(cx, cy, t);
282
283            // First-order upwind differences
284            let dphi_x = if u >= 0.0 {
285                let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
286                (phi_c - phi_xm) / dx
287            } else {
288                let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
289                (phi_xp - phi_c) / dx
290            };
291            let dphi_y = if v_y >= 0.0 {
292                let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
293                (phi_c - phi_ym) / dy
294            } else {
295                let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
296                (phi_yp - phi_c) / dy
297            };
298
299            let new_phi = phi_c - dt * (u * dphi_x + v_y * dphi_y);
300            updates.push((id, new_phi));
301        }
302
303        for (id, new_phi) in updates {
304            if let Some(cell) = self.tree.cells.get_mut(&id) {
305                cell.values[0] = new_phi;
306            }
307        }
308    }
309
310    /// Return the IDs of all *interface cells*: leaf cells where φ changes
311    /// sign among the cell and its face-neighbours.
312    ///
313    /// Equivalently: cells where `|φ| < delta` **and** at least one neighbour
314    /// has the opposite sign of φ.
315    pub fn interface_cells(&self) -> Vec<CellId> {
316        let snap = self.phi_snapshot();
317        let mut result = Vec::new();
318
319        for (&id, &phi_c) in &snap {
320            if phi_c.abs() >= self.delta {
321                continue;
322            }
323            let nbrs = self.tree.neighbors_of(id);
324            let crosses_zero = nbrs.iter().any(|&nbr| {
325                let phi_n = Self::get_phi_or_ancestor(&snap, &self.tree, nbr);
326                phi_c * phi_n < 0.0
327            });
328            if crosses_zero {
329                result.push(id);
330            }
331        }
332        result
333    }
334
335    /// Compute the first-order centred gradient `(∂φ/∂x, ∂φ/∂y)` at a leaf cell.
336    pub fn gradient(&self, id: CellId) -> Option<(f64, f64)> {
337        let snap = self.phi_snapshot();
338        let phi_c = *snap.get(&id)?;
339        let lv = id.level();
340        let dx = self.tree.dx_at_level(lv);
341        let dy = self.tree.dy_at_level(lv);
342
343        let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
344        let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
345        let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
346        let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
347
348        // Use one-sided differences at the boundary (where Neumann returns phi_c)
349        let gx = if (phi_xm - phi_c).abs() < 1e-100 && (phi_xp - phi_c).abs() < 1e-100 {
350            0.0
351        } else {
352            (phi_xp - phi_xm) / (2.0 * dx)
353        };
354        let gy = if (phi_ym - phi_c).abs() < 1e-100 && (phi_yp - phi_c).abs() < 1e-100 {
355            0.0
356        } else {
357            (phi_yp - phi_ym) / (2.0 * dy)
358        };
359        Some((gx, gy))
360    }
361
362    /// Return the minimum cell spacing (finest leaf) in x.
363    fn min_dx(&self) -> f64 {
364        self.tree
365            .leaves()
366            .iter()
367            .map(|id| self.tree.dx_at_level(id.level()))
368            .fold(f64::INFINITY, f64::min)
369    }
370
371    /// Statistics: number of narrow-band leaf cells.
372    pub fn narrow_band_count(&self) -> usize {
373        self.tree
374            .leaves()
375            .iter()
376            .filter(|&&id| {
377                self.tree
378                    .cells
379                    .get(&id)
380                    .map(|c| c.values[0].abs() < self.delta)
381                    .unwrap_or(false)
382            })
383            .count()
384    }
385
386    /// Retrieve the `CellData` of a leaf.
387    pub fn cell_data(&self, id: CellId) -> Option<&CellData> {
388        self.tree.cells.get(&id)
389    }
390}
391
392// ─────────────────────────────────────────────────────────────────────────────
393// Tests
394// ─────────────────────────────────────────────────────────────────────────────
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    /// Build a uniform-level-k quad-tree over [−1,1]×[−1,1].
401    fn uniform_tree(k: u8, n_vars: usize) -> QuadTree {
402        let mut tree = QuadTree::new([-1.0, 1.0, -1.0, 1.0], k, n_vars);
403        for _ in 0..k {
404            let current_leaves: Vec<_> = tree.leaves();
405            for id in current_leaves {
406                tree.refine_cell(id);
407            }
408        }
409        tree
410    }
411
412    #[test]
413    fn test_level_set_circle_signs() {
414        // Circle of radius 0.5 centered at (0,0): φ = |r| − 0.5
415        let tree = uniform_tree(2, 1);
416        let ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
417        // Cell at the origin should have φ < 0 (inside)
418        let near_origin = ls
419            .tree
420            .leaves()
421            .iter()
422            .find(|&&id| {
423                ls.tree
424                    .cells
425                    .get(&id)
426                    .map(|c| {
427                        let (cx, cy) = c.center(&ls.tree.domain);
428                        cx.abs() < 0.25 && cy.abs() < 0.25
429                    })
430                    .unwrap_or(false)
431            })
432            .copied();
433        if let Some(id) = near_origin {
434            assert!(ls.phi(id).unwrap_or(0.0) < 0.0, "inside circle → φ < 0");
435        }
436        // Cell at corner (0.9,0.9) should have φ > 0 (outside)
437        let far_corner = ls
438            .tree
439            .leaves()
440            .iter()
441            .find(|&&id| {
442                ls.tree
443                    .cells
444                    .get(&id)
445                    .map(|c| {
446                        let (cx, cy) = c.center(&ls.tree.domain);
447                        cx > 0.7 && cy > 0.7
448                    })
449                    .unwrap_or(false)
450            })
451            .copied();
452        if let Some(id) = far_corner {
453            assert!(ls.phi(id).unwrap_or(0.0) > 0.0, "outside circle → φ > 0");
454        }
455    }
456
457    #[test]
458    fn test_reinitialize_preserves_zero_set() {
459        let tree = uniform_tree(3, 1);
460        let mut ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
461        // Record interface cell positions before reinitialization
462        let ifaces_before: Vec<_> = ls.interface_cells();
463        assert!(!ifaces_before.is_empty(), "should have interface cells");
464
465        ls.reinitialize(5);
466
467        // After reinitialization the zero level-set should not move much
468        // (check that interface cells still have small |φ|)
469        let ifaces_after = ls.interface_cells();
470        for id in &ifaces_after {
471            let phi = ls.phi(*id).unwrap_or(f64::INFINITY);
472            // Interface cells should have |φ| < 2*dx at the finest level
473            let dx = ls.tree.dx_at_level(id.level());
474            assert!(
475                phi.abs() < 2.0 * dx + 1e-6,
476                "interface cell has |φ|={} > 2*dx={} after reinit",
477                phi.abs(),
478                2.0 * dx
479            );
480        }
481    }
482
483    #[test]
484    fn test_interface_cells_small_phi() {
485        let tree = uniform_tree(3, 1);
486        let ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
487        let ifaces = ls.interface_cells();
488        assert!(!ifaces.is_empty(), "circle should produce interface cells");
489        for id in &ifaces {
490            let phi = ls.phi(*id).unwrap_or(f64::INFINITY);
491            let dx = ls.tree.dx_at_level(id.level());
492            assert!(
493                phi.abs() < 2.0 * dx + 1e-6,
494                "interface cell |φ|={} should be < 2*dx={}",
495                phi.abs(),
496                2.0 * dx
497            );
498        }
499    }
500
501    #[test]
502    fn test_advect_translates_interface() {
503        // Constant velocity field u=1, v=0: interface should translate right
504        let tree = uniform_tree(3, 1);
505        let mut ls = LevelSet::new(tree, |x, _y| x); // vertical interface at x=0
506        let leaves = ls.tree.leaves();
507        let dt = 0.05;
508        // After 1 step with u=1 the zero-set should move from x=0 to x≈dt
509        ls.advect(&|_, _, _| 1.0, &|_, _, _| 0.0, 0.0, dt);
510        // Check that cells near x=dt have φ near 0
511        let near_dt: Vec<_> = leaves
512            .iter()
513            .filter(|&&id| {
514                ls.tree
515                    .cells
516                    .get(&id)
517                    .map(|c| {
518                        let (cx, _) = c.center(&ls.tree.domain);
519                        (cx - dt).abs() < 0.15
520                    })
521                    .unwrap_or(false)
522            })
523            .collect();
524        let any_small: bool = near_dt
525            .iter()
526            .any(|&&id| ls.phi(id).map(|phi| phi.abs() < 0.25).unwrap_or(false));
527        assert!(any_small, "interface should have moved towards x=dt");
528    }
529
530    #[test]
531    fn test_narrow_band_count() {
532        // Use a deeper tree (level 4 → 16×16 = 256 leaves) and a small delta
533        // so the narrow band does not span the entire domain.
534        let tree = uniform_tree(4, 1);
535        let mut ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
536        // Override delta to 3 × finest cell size (level-4 dx = 2/16 = 0.125)
537        let fine_dx = ls.tree.dx_at_level(4);
538        ls.set_delta(3.0 * fine_dx);
539        let nb = ls.narrow_band_count();
540        assert!(nb > 0, "should have narrow-band cells around the circle");
541        let total = ls.tree.leaves().len();
542        assert!(
543            nb < total,
544            "narrow band ({nb}) should be smaller than total leaves ({total})"
545        );
546    }
547
548    #[test]
549    fn test_sign_smooth_properties() {
550        // sign(+) > 0, sign(-) < 0, sign(0) = 0
551        assert!(sign_smooth(1.0, 0.1) > 0.0);
552        assert!(sign_smooth(-1.0, 0.1) < 0.0);
553        assert_eq!(sign_smooth(0.0, 0.1), 0.0);
554    }
555}