use crate::math::{Fix128, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Debug)]
pub struct Grid3d {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub dx: Fix128,
pub data: Vec<Fix128>,
}
impl Grid3d {
#[must_use]
pub fn new(nx: usize, ny: usize, nz: usize, dx: Fix128, value: Fix128) -> Self {
Self {
nx,
ny,
nz,
dx,
data: vec![value; nx * ny * nz],
}
}
#[inline]
#[must_use]
pub const fn idx(&self, i: usize, j: usize, k: usize) -> usize {
i + self.nx * (j + self.ny * k)
}
#[must_use]
pub fn get(&self, i: usize, j: usize, k: usize) -> Fix128 {
if i >= self.nx || j >= self.ny || k >= self.nz {
return Fix128::ZERO;
}
self.data[self.idx(i, j, k)]
}
pub fn set(&mut self, i: usize, j: usize, k: usize, v: Fix128) {
if i >= self.nx || j >= self.ny || k >= self.nz {
return;
}
let ix = self.idx(i, j, k);
self.data[ix] = v;
}
#[inline]
#[must_use]
pub fn total(&self) -> usize {
self.nx * self.ny * self.nz
}
}
pub(crate) fn advect_vof_uniform(
field: &mut Grid3d,
ux_m_per_s: Fix128,
uy_m_per_s: Fix128,
uz_m_per_s: Fix128,
dt_s: Fix128,
) {
let mut next = field.data.clone();
let sign_x = ux_m_per_s > Fix128::ZERO;
let sign_y = uy_m_per_s > Fix128::ZERO;
let sign_z = uz_m_per_s > Fix128::ZERO;
let abs_ux = ux_m_per_s.abs();
let abs_uy = uy_m_per_s.abs();
let abs_uz = uz_m_per_s.abs();
if field.dx.is_zero() {
return;
}
let cx = abs_ux * dt_s / field.dx;
let cy = abs_uy * dt_s / field.dx;
let cz = abs_uz * dt_s / field.dx;
for k in 0..field.nz {
for j in 0..field.ny {
for i in 0..field.nx {
let f = field.get(i, j, k);
let ix = field.idx(i, j, k);
let fu_x = if sign_x && i > 0 {
field.get(i - 1, j, k)
} else if !sign_x && i + 1 < field.nx {
field.get(i + 1, j, k)
} else {
Fix128::ZERO
};
let fu_y = if sign_y && j > 0 {
field.get(i, j - 1, k)
} else if !sign_y && j + 1 < field.ny {
field.get(i, j + 1, k)
} else {
Fix128::ZERO
};
let fu_z = if sign_z && k > 0 {
field.get(i, j, k - 1)
} else if !sign_z && k + 1 < field.nz {
field.get(i, j, k + 1)
} else {
Fix128::ZERO
};
let new = f - cx * (f - fu_x) - cy * (f - fu_y) - cz * (f - fu_z);
let clamped = if new < Fix128::ZERO {
Fix128::ZERO
} else if new > Fix128::ONE {
Fix128::ONE
} else {
new
};
next[ix] = clamped;
}
}
}
field.data = next;
}
#[must_use]
pub fn trilinear_range(field: &Grid3d, cx: Fix128, cy: Fix128, cz: Fix128) -> (Fix128, Fix128) {
if field.nx == 0 || field.ny == 0 || field.nz == 0 {
return (Fix128::ZERO, Fix128::ZERO);
}
let clamp_neg = |v: Fix128| if v.is_negative() { Fix128::ZERO } else { v };
let cxx = clamp_neg(cx);
let cyy = clamp_neg(cy);
let czz = clamp_neg(cz);
let ix = cxx.hi as usize;
let iy = cyy.hi as usize;
let iz = czz.hi as usize;
let i0 = ix.min(field.nx - 1);
let i1 = (ix + 1).min(field.nx - 1);
let j0 = iy.min(field.ny - 1);
let j1 = (iy + 1).min(field.ny - 1);
let k0 = iz.min(field.nz - 1);
let k1 = (iz + 1).min(field.nz - 1);
let corners = [
field.get(i0, j0, k0),
field.get(i1, j0, k0),
field.get(i0, j1, k0),
field.get(i1, j1, k0),
field.get(i0, j0, k1),
field.get(i1, j0, k1),
field.get(i0, j1, k1),
field.get(i1, j1, k1),
];
let mut lo = corners[0];
let mut hi = corners[0];
for &c in &corners[1..] {
if c < lo {
lo = c;
}
if c > hi {
hi = c;
}
}
(lo, hi)
}
#[must_use]
pub fn trilinear_sample(field: &Grid3d, cx: Fix128, cy: Fix128, cz: Fix128) -> Fix128 {
if field.nx == 0 || field.ny == 0 || field.nz == 0 {
return Fix128::ZERO;
}
let clamp_neg = |v: Fix128| if v.is_negative() { Fix128::ZERO } else { v };
let cxx = clamp_neg(cx);
let cyy = clamp_neg(cy);
let czz = clamp_neg(cz);
let ix = cxx.hi as usize;
let iy = cyy.hi as usize;
let iz = czz.hi as usize;
let u = Fix128 { hi: 0, lo: cxx.lo };
let v = Fix128 { hi: 0, lo: cyy.lo };
let w = Fix128 { hi: 0, lo: czz.lo };
let om_u = Fix128::ONE - u;
let om_v = Fix128::ONE - v;
let om_w = Fix128::ONE - w;
let i0 = ix.min(field.nx - 1);
let i1 = (ix + 1).min(field.nx - 1);
let j0 = iy.min(field.ny - 1);
let j1 = (iy + 1).min(field.ny - 1);
let k0 = iz.min(field.nz - 1);
let k1 = (iz + 1).min(field.nz - 1);
let c00 = field.get(i0, j0, k0) * om_u + field.get(i1, j0, k0) * u;
let c10 = field.get(i0, j1, k0) * om_u + field.get(i1, j1, k0) * u;
let c01 = field.get(i0, j0, k1) * om_u + field.get(i1, j0, k1) * u;
let c11 = field.get(i0, j1, k1) * om_u + field.get(i1, j1, k1) * u;
let c0 = c00 * om_v + c10 * v;
let c1 = c01 * om_v + c11 * v;
c0 * om_w + c1 * w
}
pub(crate) fn advect_vof_uniform_semi_lagrangian(
field: &mut Grid3d,
ux_m_per_s: Fix128,
uy_m_per_s: Fix128,
uz_m_per_s: Fix128,
dt_s: Fix128,
) {
if field.dx.is_zero() {
return;
}
let inv_dx = Fix128::ONE / field.dx;
let dx_cells = ux_m_per_s * dt_s * inv_dx;
let dy_cells = uy_m_per_s * dt_s * inv_dx;
let dz_cells = uz_m_per_s * dt_s * inv_dx;
let old = field.data.clone();
let old_grid = Grid3d {
nx: field.nx,
ny: field.ny,
nz: field.nz,
dx: field.dx,
data: old,
};
for k in 0..field.nz {
for j in 0..field.ny {
for i in 0..field.nx {
let cx = Fix128::from_int(i as i64) - dx_cells;
let cy = Fix128::from_int(j as i64) - dy_cells;
let cz = Fix128::from_int(k as i64) - dz_cells;
let val = trilinear_sample(&old_grid, cx, cy, cz);
let clamped = if val < Fix128::ZERO {
Fix128::ZERO
} else if val > Fix128::ONE {
Fix128::ONE
} else {
val
};
let ix = field.idx(i, j, k);
field.data[ix] = clamped;
}
}
}
}
#[must_use]
pub(crate) fn total_volume_vof(field: &Grid3d) -> Fix128 {
let cell_vol = field.dx * field.dx * field.dx;
let sum: Fix128 = field.data.iter().copied().fold(Fix128::ZERO, |a, b| a + b);
sum * cell_vol
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum VofScheme {
Upwind,
SemiLagrangian,
}
#[must_use]
pub fn advect_vof_rigid(
field: &mut Grid3d,
scheme: VofScheme,
velocity_m_per_s: Vec3Fix,
dt_s: Fix128,
) -> Fix128 {
let Vec3Fix { x, y, z } = velocity_m_per_s;
match scheme {
VofScheme::Upwind => {
let axes = [(x, field.nx), (y, field.ny), (z, field.nz)];
let mut steps = [0_usize; 3];
let mut carried_off = false;
for (a, &(u, extent)) in axes.iter().enumerate() {
match upwind_substeps(field.dx, u, dt_s, extent) {
Some(n) => steps[a] = n,
None => carried_off = true,
}
}
if carried_off {
field.data.iter_mut().for_each(|f| *f = Fix128::ZERO);
} else {
for (a, &(u, _)) in axes.iter().enumerate() {
if u.is_zero() {
continue;
}
let su = u / Fix128::from_int(steps[a] as i64);
let v = |axis: usize| if axis == a { su } else { Fix128::ZERO };
for _ in 0..steps[a] {
advect_vof_uniform(field, v(0), v(1), v(2), dt_s);
}
}
}
}
VofScheme::SemiLagrangian => advect_vof_uniform_semi_lagrangian(field, x, y, z, dt_s),
}
total_volume_vof(field)
}
fn upwind_substeps(dx: Fix128, u: Fix128, dt: Fix128, extent: usize) -> Option<usize> {
if dx.is_zero() {
return Some(1);
}
let travel = u.abs().checked_mul(dt.abs())?;
let c = travel / dx;
let whole = c.hi.max(0) as u64;
let n = if c.lo == 0 {
whole
} else {
whole.saturating_add(1)
};
if n > extent as u64 {
return None;
}
Some(n.max(1) as usize)
}
pub fn initialize_level_set_sphere(
field: &mut Grid3d,
cx_m: Fix128,
cy_m: Fix128,
cz_m: Fix128,
radius_m: Fix128,
) {
for k in 0..field.nz {
for j in 0..field.ny {
for i in 0..field.nx {
let x = Fix128::from_int(i as i64) * field.dx;
let y = Fix128::from_int(j as i64) * field.dx;
let z = Fix128::from_int(k as i64) * field.dx;
let dx = x - cx_m;
let dy = y - cy_m;
let dz = z - cz_m;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
field.set(i, j, k, dist - radius_m);
}
}
}
}
#[cfg(feature = "std")]
pub(crate) fn reinitialize_level_set(field: &mut Grid3d, iterations: u32) {
if field.nx < 3 || field.ny < 3 || field.nz < 3 {
return;
}
let dtau = field.dx * Fix128::from_ratio(1, 2);
for _ in 0..iterations {
let mut next = field.data.clone();
for k in 1..field.nz - 1 {
for j in 1..field.ny - 1 {
for i in 1..field.nx - 1 {
let phi = field.get(i, j, k);
let sign = if phi > Fix128::ZERO {
Fix128::ONE
} else if phi < Fix128::ZERO {
Fix128::NEG_ONE
} else {
Fix128::ZERO
};
let dpdx =
(field.get(i + 1, j, k) - field.get(i - 1, j, k)) / (field.dx + field.dx);
let dpdy =
(field.get(i, j + 1, k) - field.get(i, j - 1, k)) / (field.dx + field.dx);
let dpdz =
(field.get(i, j, k + 1) - field.get(i, j, k - 1)) / (field.dx + field.dx);
let grad_sq = dpdx * dpdx + dpdy * dpdy + dpdz * dpdz;
let grad_mag = grad_sq.sqrt();
let ix = field.idx(i, j, k);
next[ix] = phi + dtau * sign * (Fix128::ONE - grad_mag);
}
}
}
field.data = next;
}
}
#[must_use]
pub fn curvature_at(field: &Grid3d, i: usize, j: usize, k: usize) -> Fix128 {
if i == 0 || j == 0 || k == 0 || i + 1 >= field.nx || j + 1 >= field.ny || k + 1 >= field.nz {
return Fix128::ZERO;
}
let dx = field.dx;
let dx2 = dx * dx;
if dx2.is_zero() {
return Fix128::ZERO;
}
let g = |di: isize, dj: isize, dk: isize| {
field.get(
(i as isize + di) as usize,
(j as isize + dj) as usize,
(k as isize + dk) as usize,
)
};
let phi = g(0, 0, 0);
let two_dx = dx.double();
let px = (g(1, 0, 0) - g(-1, 0, 0)) / two_dx;
let py = (g(0, 1, 0) - g(0, -1, 0)) / two_dx;
let pz = (g(0, 0, 1) - g(0, 0, -1)) / two_dx;
let grad_sq = px * px + py * py + pz * pz;
if grad_sq.is_zero() {
return Fix128::ZERO;
}
let pxx = (g(1, 0, 0) - phi.double() + g(-1, 0, 0)) / dx2;
let pyy = (g(0, 1, 0) - phi.double() + g(0, -1, 0)) / dx2;
let pzz = (g(0, 0, 1) - phi.double() + g(0, 0, -1)) / dx2;
let four_dx2 = dx2 + dx2 + dx2 + dx2;
let pxy = (g(1, 1, 0) - g(1, -1, 0) - g(-1, 1, 0) + g(-1, -1, 0)) / four_dx2;
let pxz = (g(1, 0, 1) - g(1, 0, -1) - g(-1, 0, 1) + g(-1, 0, -1)) / four_dx2;
let pyz = (g(0, 1, 1) - g(0, 1, -1) - g(0, -1, 1) + g(0, -1, -1)) / four_dx2;
let laplacian = pxx + pyy + pzz;
let hess = px * px * pxx
+ py * py * pyy
+ pz * pz * pzz
+ (px * py * pxy + px * pz * pxz + py * pz * pyz).double();
let grad = grad_sq.sqrt();
(grad_sq * laplacian - hess) / (grad_sq * grad)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_new_all_default_value() {
let g = Grid3d::new(3, 3, 3, Fix128::ONE, Fix128::from_int(5));
assert_eq!(g.total(), 27);
assert_eq!(g.get(0, 0, 0), Fix128::from_int(5));
assert_eq!(g.get(2, 2, 2), Fix128::from_int(5));
}
#[test]
fn grid_out_of_range_returns_zero() {
let g = Grid3d::new(3, 3, 3, Fix128::ONE, Fix128::from_int(5));
assert_eq!(g.get(5, 0, 0), Fix128::ZERO);
}
#[test]
fn grid_set_get_roundtrip() {
let mut g = Grid3d::new(3, 3, 3, Fix128::ONE, Fix128::ZERO);
g.set(1, 1, 1, Fix128::from_int(42));
assert_eq!(g.get(1, 1, 1), Fix128::from_int(42));
}
#[test]
fn vof_uniform_advection_shifts_field() {
let mut g = Grid3d::new(4, 1, 1, Fix128::ONE, Fix128::ZERO);
g.set(0, 0, 0, Fix128::ONE);
advect_vof_uniform(
&mut g,
Fix128::ONE,
Fix128::ZERO,
Fix128::ZERO,
Fix128::from_ratio(5, 10),
);
assert!(g.get(0, 0, 0) < Fix128::ONE);
assert!(g.get(1, 0, 0) > Fix128::ZERO);
}
#[test]
fn vof_advection_conserves_volume_bounded_step() {
let mut g = Grid3d::new(6, 1, 1, Fix128::ONE, Fix128::ZERO);
g.set(1, 0, 0, Fix128::ONE);
g.set(2, 0, 0, Fix128::ONE);
let vol_initial = total_volume_vof(&g);
for _ in 0..3 {
advect_vof_uniform(
&mut g,
Fix128::from_ratio(1, 10),
Fix128::ZERO,
Fix128::ZERO,
Fix128::ONE,
);
}
let vol_after = total_volume_vof(&g);
let ratio = vol_after / vol_initial;
assert!(ratio > Fix128::from_ratio(90, 100));
assert!(ratio < Fix128::from_ratio(110, 100));
}
#[test]
fn vof_bounded_to_zero_one() {
let mut g = Grid3d::new(3, 1, 1, Fix128::ONE, Fix128::from_int(5)); advect_vof_uniform(
&mut g,
Fix128::from_ratio(1, 10),
Fix128::ZERO,
Fix128::ZERO,
Fix128::ONE,
);
for i in 0..3 {
assert!(g.get(i, 0, 0) <= Fix128::ONE);
assert!(g.get(i, 0, 0) >= Fix128::ZERO);
}
}
#[test]
fn level_set_sphere_zero_on_surface() {
let mut g = Grid3d::new(11, 11, 11, Fix128::ONE, Fix128::ZERO);
initialize_level_set_sphere(
&mut g,
Fix128::from_int(5),
Fix128::from_int(5),
Fix128::from_int(5),
Fix128::from_int(3),
);
assert_eq!(g.get(5, 5, 5), Fix128::from_int(-3));
assert_eq!(g.get(8, 5, 5), Fix128::ZERO);
assert_eq!(g.get(10, 5, 5), Fix128::from_int(2));
}
#[test]
fn total_volume_vof_uniform_fill() {
let g = Grid3d::new(4, 4, 4, Fix128::from_int(2), Fix128::ONE);
assert_eq!(total_volume_vof(&g), Fix128::from_int(512));
}
#[cfg(feature = "std")]
#[test]
fn reinitialize_level_set_smoke() {
let mut g = Grid3d::new(7, 7, 7, Fix128::ONE, Fix128::ZERO);
initialize_level_set_sphere(
&mut g,
Fix128::from_int(3),
Fix128::from_int(3),
Fix128::from_int(3),
Fix128::from_int(2),
);
reinitialize_level_set(&mut g, 3);
let phi_surface = g.get(5, 3, 3);
assert!(phi_surface.abs() < Fix128::from_int(2));
}
#[cfg(feature = "std")]
#[test]
fn reinitialize_without_an_interior_is_untouched_and_does_not_panic() {
for (nx, ny, nz) in [
(0, 0, 0),
(0, 3, 3),
(3, 0, 3),
(3, 3, 0),
(2, 3, 3),
(3, 2, 3),
(3, 3, 2),
(1, 1, 1),
] {
let mut g = Grid3d::new(nx, ny, nz, Fix128::ONE, Fix128::from_int(5));
let before = g.data.clone();
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
reinitialize_level_set(&mut g, 3);
}));
assert!(res.is_ok(), "{nx}x{ny}x{nz} panicked");
assert_eq!(g.data, before, "{nx}x{ny}x{nz}");
}
}
#[test]
fn curvature_zero_at_flat_interface() {
let g = Grid3d::new(3, 3, 3, Fix128::ONE, Fix128::from_int(5));
assert_eq!(curvature_at(&g, 1, 1, 1), Fix128::ZERO);
}
#[test]
fn semi_lagrangian_uniform_shifts_field() {
let mut g = Grid3d::new(6, 1, 1, Fix128::ONE, Fix128::ZERO);
g.set(1, 0, 0, Fix128::ONE);
advect_vof_uniform_semi_lagrangian(
&mut g,
Fix128::ONE,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ONE,
);
assert!(g.get(2, 0, 0) > Fix128::from_ratio(90, 100));
}
#[test]
fn trilinear_sample_at_grid_point_returns_value() {
let mut g = Grid3d::new(3, 3, 3, Fix128::ONE, Fix128::ZERO);
g.set(1, 1, 1, Fix128::from_int(5));
let v = trilinear_sample(
&g,
Fix128::from_int(1),
Fix128::from_int(1),
Fix128::from_int(1),
);
assert_eq!(v, Fix128::from_int(5));
}
#[test]
fn semi_lagrangian_zero_velocity_preserves_field() {
let mut g = Grid3d::new(4, 4, 4, Fix128::ONE, Fix128::from_ratio(3, 10));
let before = g.data.clone();
advect_vof_uniform_semi_lagrangian(
&mut g,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ONE,
);
assert_eq!(g.data, before);
}
#[test]
fn curvature_bounded_at_edges() {
let mut g = Grid3d::new(5, 1, 1, Fix128::ONE, Fix128::ZERO);
for i in 0..5 {
g.set(i, 0, 0, Fix128::from_int(i as i64));
}
assert_eq!(curvature_at(&g, 0, 0, 0), Fix128::ZERO);
}
fn single_cell(n: usize, i: usize, j: usize, k: usize) -> Grid3d {
let mut g = Grid3d::new(n, n, n, Fix128::ONE, Fix128::ZERO);
g.set(i, j, k, Fix128::ONE);
g
}
fn occupied(g: &Grid3d) -> Vec<(usize, Fix128)> {
g.data
.iter()
.copied()
.enumerate()
.filter(|&(_, f)| !f.is_zero())
.collect()
}
#[test]
fn rigid_upwind_moves_a_c_cell_by_whole_cells_on_every_axis() {
let mut g = single_cell(6, 1, 3, 2);
let v = advect_vof_rigid(
&mut g,
VofScheme::Upwind,
Vec3Fix::from_int(2, -1, 1),
Fix128::ONE,
);
assert_eq!(v, Fix128::ONE);
assert_eq!(occupied(&g), vec![(g.idx(3, 2, 3), Fix128::ONE)]);
}
#[test]
fn rigid_upwind_fractional_courant_conserves_mass() {
let mut g = single_cell(8, 2, 4, 4);
let v = advect_vof_rigid(
&mut g,
VofScheme::Upwind,
Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO),
Fix128::ONE,
);
assert_eq!(v, Fix128::ONE);
assert_eq!(
occupied(&g),
vec![
(g.idx(2, 4, 4), Fix128::from_ratio(1, 16)),
(g.idx(3, 4, 4), Fix128::from_ratio(3, 8)),
(g.idx(4, 4, 4), Fix128::from_ratio(9, 16)),
]
);
}
#[test]
fn rigid_upwind_past_the_grid_or_the_range_empties_the_field() {
let mut g = single_cell(6, 0, 0, 0);
let v = advect_vof_rigid(
&mut g,
VofScheme::Upwind,
Vec3Fix::from_int(0, 7, 0),
Fix128::ONE,
);
assert_eq!(v, Fix128::ZERO);
assert!(occupied(&g).is_empty());
let mut g = single_cell(6, 0, 0, 0);
let huge = Fix128::from_int(i64::MAX >> 1);
let v = advect_vof_rigid(
&mut g,
VofScheme::Upwind,
Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, huge),
Fix128::from_int(4),
);
assert_eq!(v, Fix128::ZERO);
}
#[test]
fn rigid_zero_spacing_leaves_the_field_untouched() {
let mut g = Grid3d::new(3, 3, 3, Fix128::ZERO, Fix128::from_int(2));
for scheme in [VofScheme::Upwind, VofScheme::SemiLagrangian] {
let v = advect_vof_rigid(&mut g, scheme, Vec3Fix::from_int(1, 1, 1), Fix128::ONE);
assert_eq!(v, Fix128::ZERO);
assert!(g.data.iter().all(|&f| f == Fix128::from_int(2)));
}
}
#[test]
fn rigid_semi_lagrangian_translates_by_the_integer_displacement() {
let mut g = single_cell(6, 2, 2, 4);
let v = advect_vof_rigid(
&mut g,
VofScheme::SemiLagrangian,
Vec3Fix::from_int(1, 0, -2),
Fix128::ONE,
);
assert_eq!(v, Fix128::ONE);
assert_eq!(occupied(&g), vec![(g.idx(3, 2, 2), Fix128::ONE)]);
}
#[test]
fn trilinear_range_is_the_min_max_of_the_eight_corners() {
let mut g = Grid3d::new(2, 2, 2, Fix128::ONE, Fix128::ZERO);
for k in 0..2 {
for j in 0..2 {
for i in 0..2 {
g.set(i, j, k, Fix128::from_int((i + 2 * j + 4 * k) as i64));
}
}
}
let h = Fix128::from_ratio(1, 2);
assert_eq!(
trilinear_range(&g, h, h, h),
(Fix128::ZERO, Fix128::from_int(7))
);
assert_eq!(trilinear_sample(&g, h, h, h), Fix128::from_ratio(7, 2));
let far = Fix128::from_int(9);
assert_eq!(
trilinear_range(&g, -far, far, far),
(Fix128::from_int(6), Fix128::from_int(7))
);
let empty = Grid3d::new(0, 2, 2, Fix128::ONE, Fix128::ONE);
assert_eq!(
trilinear_range(&empty, h, h, h),
(Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(trilinear_sample(&empty, h, h, h), Fix128::ZERO);
}
#[test]
fn curvature_of_a_sphere_is_two_over_r_and_scale_free() {
let dx = Fix128::from_ratio(1, 10);
let mut g = Grid3d::new(21, 21, 21, dx, Fix128::ZERO);
let one = Fix128::ONE;
initialize_level_set_sphere(&mut g, one, one, one, Fix128::from_ratio(1, 2));
let k = curvature_at(&g, 15, 10, 10).to_f64();
assert!((k - 4.0).abs() < 4.0 * 0.05, "κ = {k}");
let mut g2 = g.clone();
g2.data.iter_mut().for_each(|f| *f = f.double());
let k2 = curvature_at(&g2, 15, 10, 10).to_f64();
assert!((k2 - k).abs() < 1e-12, "{k2} vs {k}");
let flat = Grid3d::new(3, 3, 3, dx, one);
assert_eq!(curvature_at(&flat, 1, 1, 1), Fix128::ZERO);
let zero_dx = Grid3d::new(3, 3, 3, Fix128::ZERO, one);
assert_eq!(curvature_at(&zero_dx, 1, 1, 1), Fix128::ZERO);
}
}