use crate::amr::quadtree::{CellData, CellId, Morton2D, QuadTree};
use std::collections::HashMap;
#[inline]
fn sign_smooth(phi: f64, eps: f64) -> f64 {
phi / (phi * phi + eps * eps).sqrt()
}
fn godunov_gradient_magnitude(
phi_c: f64,
phi_xm: f64,
phi_xp: f64,
phi_ym: f64,
phi_yp: f64,
dx: f64,
dy: f64,
sign: f64,
) -> f64 {
let dm_x = (phi_c - phi_xm) / dx;
let dp_x = (phi_xp - phi_c) / dx;
let dm_y = (phi_c - phi_ym) / dy;
let dp_y = (phi_yp - phi_c) / dy;
let ax = if sign > 0.0 {
f64::max(f64::max(dm_x, 0.0).powi(2), f64::min(dp_x, 0.0).powi(2))
} else {
f64::max(f64::min(dm_x, 0.0).powi(2), f64::max(dp_x, 0.0).powi(2))
};
let ay = if sign > 0.0 {
f64::max(f64::max(dm_y, 0.0).powi(2), f64::min(dp_y, 0.0).powi(2))
} else {
f64::max(f64::min(dm_y, 0.0).powi(2), f64::max(dp_y, 0.0).powi(2))
};
(ax + ay).sqrt()
}
pub struct LevelSet {
pub tree: QuadTree,
pub delta: f64,
}
impl LevelSet {
pub fn new(tree: QuadTree, phi0: impl Fn(f64, f64) -> f64) -> Self {
let mut ls = LevelSet { tree, delta: 3.0 };
let leaves: Vec<CellId> = ls.tree.leaves();
for id in leaves {
if let Some(cell) = ls.tree.cells.get(&id) {
let (cx, cy) = cell.center(&ls.tree.domain);
let phi = phi0(cx, cy);
let n_vars = ls.tree.n_vars;
let mut vals = vec![0.0f64; n_vars];
vals[0] = phi;
if let Some(cell) = ls.tree.cells.get_mut(&id) {
cell.values = vals;
}
}
}
let coarsest_dx = ls.tree.dx_at_level(0);
ls.delta = 3.0 * coarsest_dx;
ls
}
pub fn set_delta(&mut self, delta: f64) {
self.delta = delta;
}
pub fn phi(&self, id: CellId) -> Option<f64> {
self.tree.cells.get(&id).map(|c| c.values[0])
}
fn phi_snapshot(&self) -> HashMap<CellId, f64> {
self.tree
.leaves()
.into_iter()
.filter_map(|id| {
let phi = self.tree.cells.get(&id)?.values[0];
Some((id, phi))
})
.collect()
}
fn get_phi_or_ancestor(snap: &HashMap<CellId, f64>, tree: &QuadTree, id: CellId) -> f64 {
if let Some(&phi) = snap.get(&id) {
return phi;
}
let mut cur = id;
loop {
match cur.parent() {
None => return 0.0,
Some(p) => {
if let Some(&phi) = snap.get(&p) {
return phi;
}
cur = p;
}
}
}
}
fn neighbour_phi(&self, id: CellId, snap: &HashMap<CellId, f64>, di: i64, dj: i64) -> f64 {
let lv = id.level();
let size = 1u32.checked_shl(lv as u32).unwrap_or(u32::MAX);
let (ix, iy) = id.grid_coords();
let ni = ix as i64 + di;
let nj = iy as i64 + dj;
if ni < 0 || nj < 0 || ni >= size as i64 || nj >= size as i64 {
return snap.get(&id).copied().unwrap_or(0.0);
}
let nbr_id = CellId::new(lv, Morton2D::encode(ni as u32, nj as u32));
Self::get_phi_or_ancestor(snap, &self.tree, nbr_id)
}
pub fn reinitialize(&mut self, n_iters: usize) {
for _ in 0..n_iters {
let snap = self.phi_snapshot();
let leaves: Vec<CellId> = self.tree.leaves().into_iter().collect();
let mut updates: Vec<(CellId, f64)> = Vec::new();
for &id in &leaves {
let phi_c = match snap.get(&id) {
Some(&v) => v,
None => continue,
};
if phi_c.abs() >= self.delta {
continue;
}
let lv = id.level();
let dx = self.tree.dx_at_level(lv);
let dy = self.tree.dy_at_level(lv);
let eps = 0.5 * dx.min(dy);
let sign0 = sign_smooth(phi_c, eps);
let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
let grad_mag = godunov_gradient_magnitude(
phi_c, phi_xm, phi_xp, phi_ym, phi_yp, dx, dy, sign0,
);
let dt = 0.5 * dx.min(dy);
let new_phi = phi_c - dt * sign0 * (grad_mag - 1.0);
updates.push((id, new_phi));
}
for (id, new_phi) in updates {
if let Some(cell) = self.tree.cells.get_mut(&id) {
cell.values[0] = new_phi;
}
}
}
}
pub fn advect(
&mut self,
vx: &dyn Fn(f64, f64, f64) -> f64,
vy: &dyn Fn(f64, f64, f64) -> f64,
t: f64,
dt: f64,
) {
let snap = self.phi_snapshot();
let leaves: Vec<CellId> = self.tree.leaves();
let mut updates: Vec<(CellId, f64)> = Vec::new();
for &id in &leaves {
let phi_c = match snap.get(&id) {
Some(&v) => v,
None => continue,
};
if phi_c.abs() >= self.delta {
continue;
}
let lv = id.level();
let dx = self.tree.dx_at_level(lv);
let dy = self.tree.dy_at_level(lv);
let cell = match self.tree.cells.get(&id) {
Some(c) => c,
None => continue,
};
let (cx, cy) = cell.center(&self.tree.domain);
let u = vx(cx, cy, t);
let v_y = vy(cx, cy, t);
let dphi_x = if u >= 0.0 {
let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
(phi_c - phi_xm) / dx
} else {
let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
(phi_xp - phi_c) / dx
};
let dphi_y = if v_y >= 0.0 {
let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
(phi_c - phi_ym) / dy
} else {
let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
(phi_yp - phi_c) / dy
};
let new_phi = phi_c - dt * (u * dphi_x + v_y * dphi_y);
updates.push((id, new_phi));
}
for (id, new_phi) in updates {
if let Some(cell) = self.tree.cells.get_mut(&id) {
cell.values[0] = new_phi;
}
}
}
pub fn interface_cells(&self) -> Vec<CellId> {
let snap = self.phi_snapshot();
let mut result = Vec::new();
for (&id, &phi_c) in &snap {
if phi_c.abs() >= self.delta {
continue;
}
let nbrs = self.tree.neighbors_of(id);
let crosses_zero = nbrs.iter().any(|&nbr| {
let phi_n = Self::get_phi_or_ancestor(&snap, &self.tree, nbr);
phi_c * phi_n < 0.0
});
if crosses_zero {
result.push(id);
}
}
result
}
pub fn gradient(&self, id: CellId) -> Option<(f64, f64)> {
let snap = self.phi_snapshot();
let phi_c = *snap.get(&id)?;
let lv = id.level();
let dx = self.tree.dx_at_level(lv);
let dy = self.tree.dy_at_level(lv);
let phi_xm = self.neighbour_phi(id, &snap, -1, 0);
let phi_xp = self.neighbour_phi(id, &snap, 1, 0);
let phi_ym = self.neighbour_phi(id, &snap, 0, -1);
let phi_yp = self.neighbour_phi(id, &snap, 0, 1);
let gx = if (phi_xm - phi_c).abs() < 1e-100 && (phi_xp - phi_c).abs() < 1e-100 {
0.0
} else {
(phi_xp - phi_xm) / (2.0 * dx)
};
let gy = if (phi_ym - phi_c).abs() < 1e-100 && (phi_yp - phi_c).abs() < 1e-100 {
0.0
} else {
(phi_yp - phi_ym) / (2.0 * dy)
};
Some((gx, gy))
}
fn min_dx(&self) -> f64 {
self.tree
.leaves()
.iter()
.map(|id| self.tree.dx_at_level(id.level()))
.fold(f64::INFINITY, f64::min)
}
pub fn narrow_band_count(&self) -> usize {
self.tree
.leaves()
.iter()
.filter(|&&id| {
self.tree
.cells
.get(&id)
.map(|c| c.values[0].abs() < self.delta)
.unwrap_or(false)
})
.count()
}
pub fn cell_data(&self, id: CellId) -> Option<&CellData> {
self.tree.cells.get(&id)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform_tree(k: u8, n_vars: usize) -> QuadTree {
let mut tree = QuadTree::new([-1.0, 1.0, -1.0, 1.0], k, n_vars);
for _ in 0..k {
let current_leaves: Vec<_> = tree.leaves();
for id in current_leaves {
tree.refine_cell(id);
}
}
tree
}
#[test]
fn test_level_set_circle_signs() {
let tree = uniform_tree(2, 1);
let ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
let near_origin = ls
.tree
.leaves()
.iter()
.find(|&&id| {
ls.tree
.cells
.get(&id)
.map(|c| {
let (cx, cy) = c.center(&ls.tree.domain);
cx.abs() < 0.25 && cy.abs() < 0.25
})
.unwrap_or(false)
})
.copied();
if let Some(id) = near_origin {
assert!(ls.phi(id).unwrap_or(0.0) < 0.0, "inside circle → φ < 0");
}
let far_corner = ls
.tree
.leaves()
.iter()
.find(|&&id| {
ls.tree
.cells
.get(&id)
.map(|c| {
let (cx, cy) = c.center(&ls.tree.domain);
cx > 0.7 && cy > 0.7
})
.unwrap_or(false)
})
.copied();
if let Some(id) = far_corner {
assert!(ls.phi(id).unwrap_or(0.0) > 0.0, "outside circle → φ > 0");
}
}
#[test]
fn test_reinitialize_preserves_zero_set() {
let tree = uniform_tree(3, 1);
let mut ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
let ifaces_before: Vec<_> = ls.interface_cells();
assert!(!ifaces_before.is_empty(), "should have interface cells");
ls.reinitialize(5);
let ifaces_after = ls.interface_cells();
for id in &ifaces_after {
let phi = ls.phi(*id).unwrap_or(f64::INFINITY);
let dx = ls.tree.dx_at_level(id.level());
assert!(
phi.abs() < 2.0 * dx + 1e-6,
"interface cell has |φ|={} > 2*dx={} after reinit",
phi.abs(),
2.0 * dx
);
}
}
#[test]
fn test_interface_cells_small_phi() {
let tree = uniform_tree(3, 1);
let ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
let ifaces = ls.interface_cells();
assert!(!ifaces.is_empty(), "circle should produce interface cells");
for id in &ifaces {
let phi = ls.phi(*id).unwrap_or(f64::INFINITY);
let dx = ls.tree.dx_at_level(id.level());
assert!(
phi.abs() < 2.0 * dx + 1e-6,
"interface cell |φ|={} should be < 2*dx={}",
phi.abs(),
2.0 * dx
);
}
}
#[test]
fn test_advect_translates_interface() {
let tree = uniform_tree(3, 1);
let mut ls = LevelSet::new(tree, |x, _y| x); let leaves = ls.tree.leaves();
let dt = 0.05;
ls.advect(&|_, _, _| 1.0, &|_, _, _| 0.0, 0.0, dt);
let near_dt: Vec<_> = leaves
.iter()
.filter(|&&id| {
ls.tree
.cells
.get(&id)
.map(|c| {
let (cx, _) = c.center(&ls.tree.domain);
(cx - dt).abs() < 0.15
})
.unwrap_or(false)
})
.collect();
let any_small: bool = near_dt
.iter()
.any(|&&id| ls.phi(id).map(|phi| phi.abs() < 0.25).unwrap_or(false));
assert!(any_small, "interface should have moved towards x=dt");
}
#[test]
fn test_narrow_band_count() {
let tree = uniform_tree(4, 1);
let mut ls = LevelSet::new(tree, |x, y| (x * x + y * y).sqrt() - 0.5);
let fine_dx = ls.tree.dx_at_level(4);
ls.set_delta(3.0 * fine_dx);
let nb = ls.narrow_band_count();
assert!(nb > 0, "should have narrow-band cells around the circle");
let total = ls.tree.leaves().len();
assert!(
nb < total,
"narrow band ({nb}) should be smaller than total leaves ({total})"
);
}
#[test]
fn test_sign_smooth_properties() {
assert!(sign_smooth(1.0, 0.1) > 0.0);
assert!(sign_smooth(-1.0, 0.1) < 0.0);
assert_eq!(sign_smooth(0.0, 0.1), 0.0);
}
}