use serde::{Deserialize, Serialize};
use crate::error::{PravashError, Result};
use tracing::trace_span;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VofField {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub alpha: Vec<f64>,
}
impl VofField {
pub fn new(nx: usize, ny: usize, dx: f64) -> Result<Self> {
if nx < 4 || ny < 4 {
return Err(PravashError::InvalidGridResolution { nx, ny });
}
if dx <= 0.0 || !dx.is_finite() {
return Err(PravashError::InvalidParameter {
reason: format!("cell size must be positive: {dx}").into(),
});
}
Ok(Self {
nx,
ny,
dx,
alpha: vec![0.0; nx * ny],
})
}
#[inline]
#[must_use]
pub fn is_surface(&self, x: usize, y: usize) -> bool {
self.is_surface_with_threshold(x, y, 1e-6)
}
#[inline]
#[must_use]
pub fn is_surface_with_threshold(&self, x: usize, y: usize, threshold: f64) -> bool {
let a = self.alpha[y * self.nx + x];
a > threshold && a < 1.0 - threshold
}
#[inline]
#[must_use]
pub fn is_full(&self, x: usize, y: usize) -> bool {
self.is_full_with_threshold(x, y, 1e-6)
}
#[inline]
#[must_use]
pub fn is_full_with_threshold(&self, x: usize, y: usize, threshold: f64) -> bool {
self.alpha[y * self.nx + x] > 1.0 - threshold
}
#[inline]
#[must_use]
pub fn is_empty(&self, x: usize, y: usize) -> bool {
self.is_empty_with_threshold(x, y, 1e-6)
}
#[inline]
#[must_use]
pub fn is_empty_with_threshold(&self, x: usize, y: usize, threshold: f64) -> bool {
self.alpha[y * self.nx + x] < threshold
}
pub fn fill_rect(&mut self, x_min: f64, y_min: f64, x_max: f64, y_max: f64) {
for y in 0..self.ny {
for x in 0..self.nx {
let px = x as f64 * self.dx;
let py = y as f64 * self.dx;
if px >= x_min && px <= x_max && py >= y_min && py <= y_max {
self.alpha[y * self.nx + x] = 1.0;
}
}
}
}
pub fn fill_circle(&mut self, cx: f64, cy: f64, radius: f64) {
let r2 = radius * radius;
for y in 0..self.ny {
for x in 0..self.nx {
let px = x as f64 * self.dx;
let py = y as f64 * self.dx;
let dx = px - cx;
let dy = py - cy;
if dx * dx + dy * dy < r2 {
self.alpha[y * self.nx + x] = 1.0;
}
}
}
}
#[must_use]
pub fn total_volume(&self) -> f64 {
let cell_area = self.dx * self.dx;
self.alpha.iter().map(|&a| a * cell_area).sum()
}
pub fn advect(&mut self, vx: &[f64], vy: &[f64], dt: f64) -> Result<()> {
let _span = trace_span!("vof::advect", nx = self.nx, ny = self.ny).entered();
if !dt.is_finite() || dt <= 0.0 {
return Err(PravashError::InvalidTimestep { dt });
}
let nx = self.nx;
let ny = self.ny;
let dx = self.dx;
let inv_dx = 1.0 / dx;
let old_alpha = self.alpha.clone();
for y in 1..ny - 1 {
for x in 1..nx - 1 {
let i = y * nx + x;
let flux_right = {
let u = 0.5 * (vx[i] + vx[i + 1]);
let donor = if u > 0.0 {
old_alpha[i]
} else {
old_alpha[i + 1]
};
u * donor * dt * inv_dx
};
let flux_left = {
let u = 0.5 * (vx[i - 1] + vx[i]);
let donor = if u > 0.0 {
old_alpha[i - 1]
} else {
old_alpha[i]
};
u * donor * dt * inv_dx
};
let flux_top = {
let v = 0.5 * (vy[i] + vy[i + nx]);
let donor = if v > 0.0 {
old_alpha[i]
} else {
old_alpha[i + nx]
};
v * donor * dt * inv_dx
};
let flux_bottom = {
let v = 0.5 * (vy[i - nx] + vy[i]);
let donor = if v > 0.0 {
old_alpha[i - nx]
} else {
old_alpha[i]
};
v * donor * dt * inv_dx
};
self.alpha[i] -= flux_right - flux_left + flux_top - flux_bottom;
self.alpha[i] = self.alpha[i].clamp(0.0, 1.0);
}
}
Ok(())
}
#[must_use]
pub fn interface_normal(&self, x: usize, y: usize) -> (f64, f64) {
if x == 0 || x >= self.nx - 1 || y == 0 || y >= self.ny - 1 {
return (0.0, 0.0);
}
let i = y * self.nx + x;
let inv_2dx = 0.5 / self.dx;
let gx = (self.alpha[i + 1] - self.alpha[i - 1]) * inv_2dx;
let gy = (self.alpha[i + self.nx] - self.alpha[i - self.nx]) * inv_2dx;
let mag = (gx * gx + gy * gy).sqrt();
if mag < 1e-20 {
(0.0, 0.0)
} else {
(gx / mag, gy / mag)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vof_new() {
let vof = VofField::new(10, 10, 0.1).unwrap();
assert_eq!(vof.alpha.len(), 100);
assert!(vof.is_empty(5, 5));
}
#[test]
fn test_vof_fill_rect() {
let mut vof = VofField::new(10, 10, 0.1).unwrap();
vof.fill_rect(0.2, 0.2, 0.5, 0.5);
assert!(vof.is_full(3, 3));
assert!(vof.is_empty(0, 0));
}
#[test]
fn test_vof_fill_circle() {
let mut vof = VofField::new(20, 20, 0.1).unwrap();
vof.fill_circle(1.0, 1.0, 0.3);
assert!(vof.is_full(10, 10));
assert!(vof.is_empty(0, 0));
}
#[test]
fn test_vof_total_volume() {
let mut vof = VofField::new(10, 10, 0.1).unwrap();
vof.alpha.fill(1.0);
let expected = 10.0 * 10.0 * 0.1 * 0.1;
assert!((vof.total_volume() - expected).abs() < 1e-6);
}
#[test]
fn test_vof_advect_stationary() {
let mut vof = VofField::new(10, 10, 0.1).unwrap();
vof.fill_rect(0.3, 0.3, 0.6, 0.6);
let vol_before = vof.total_volume();
let zero_vel = vec![0.0; 100];
vof.advect(&zero_vel, &zero_vel, 0.01).unwrap();
let vol_after = vof.total_volume();
assert!((vol_before - vol_after).abs() < 1e-10);
}
#[test]
fn test_vof_interface_normal() {
let mut vof = VofField::new(20, 20, 0.1).unwrap();
for y in 0..20 {
for x in 0..10 {
vof.alpha[y * 20 + x] = 1.0;
}
}
let (nx, _ny) = vof.interface_normal(10, 10);
assert!(nx.abs() > 0.5, "should have strong x-component: nx={nx}");
}
}