use std::f64::consts::PI;
#[inline]
fn vec_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn vec_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn vec_scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn vec_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn vec_len(a: [f64; 3]) -> f64 {
vec_dot(a, a).sqrt()
}
#[inline]
fn vec_normalize(a: [f64; 3]) -> [f64; 3] {
let l = vec_len(a);
if l < 1e-15 {
[0.0; 3]
} else {
vec_scale(a, 1.0 / l)
}
}
const G_ACCEL: f64 = 9.80665;
const RHO_WATER: f64 = 998.2;
const MU_WATER: f64 = 1.002e-3;
const GAMMA_WATER: f64 = 0.0728;
#[derive(Clone, Debug)]
pub struct FluidFilmProperties {
pub density: f64,
pub viscosity: f64,
pub surface_tension: f64,
pub temperature: f64,
pub contact_angle: f64,
}
impl FluidFilmProperties {
pub fn new(
density: f64,
viscosity: f64,
surface_tension: f64,
temperature: f64,
contact_angle: f64,
) -> Self {
Self {
density,
viscosity,
surface_tension,
temperature,
contact_angle,
}
}
pub fn water(contact_angle: f64) -> Self {
Self::new(RHO_WATER, MU_WATER, GAMMA_WATER, 293.15, contact_angle)
}
pub fn silicone_oil() -> Self {
Self::new(930.0, 9.3e-3, 0.021, 298.15, 0.0)
}
pub fn kinematic_viscosity(&self) -> f64 {
self.viscosity / self.density
}
pub fn capillary_length(&self) -> f64 {
(self.surface_tension / (self.density * G_ACCEL)).sqrt()
}
pub fn capillary_number(&self, velocity: f64) -> f64 {
self.viscosity * velocity / self.surface_tension
}
pub fn weber_number(&self, velocity: f64, length: f64) -> f64 {
self.density * velocity * velocity * length / self.surface_tension
}
pub fn reynolds_number(&self, velocity: f64, length: f64) -> f64 {
self.density * velocity * length / self.viscosity
}
pub fn bond_number(&self, length: f64) -> f64 {
self.density * G_ACCEL * length * length / self.surface_tension
}
pub fn ohnesorge_number(&self, length: f64) -> f64 {
self.viscosity / (self.density * self.surface_tension * length).sqrt()
}
pub fn spreading_coefficient(gamma_sv: f64, gamma_sl: f64, gamma_lv: f64) -> f64 {
gamma_sv - gamma_sl - gamma_lv
}
}
#[derive(Clone, Debug)]
pub struct ShallowWaterCell1D {
pub h: f64,
pub u: f64,
}
#[derive(Clone, Debug)]
pub struct ShallowWaterSolver1D {
pub cells: Vec<ShallowWaterCell1D>,
pub dx: f64,
pub gravity_along: f64,
pub friction: f64,
}
impl ShallowWaterSolver1D {
pub fn new(n: usize, dx: f64, h0: f64, gravity_along: f64, friction: f64) -> Self {
let cells = vec![ShallowWaterCell1D { h: h0, u: 0.0 }; n];
Self {
cells,
dx,
gravity_along,
friction,
}
}
pub fn num_cells(&self) -> usize {
self.cells.len()
}
pub fn total_volume(&self) -> f64 {
self.cells.iter().map(|c| c.h * self.dx).sum()
}
pub fn step(&mut self, dt: f64) {
let n = self.cells.len();
if n < 3 {
return;
}
let mut new_h = vec![0.0; n];
let mut new_hu = vec![0.0; n];
for i in 1..n - 1 {
let hl = self.cells[i - 1].h;
let hc = self.cells[i].h;
let hr = self.cells[i + 1].h;
let ul = self.cells[i - 1].u;
let uc = self.cells[i].u;
let ur = self.cells[i + 1].u;
let hul = hl * ul;
let _huc = hc * uc;
let hur = hr * ur;
let fh_l = hul;
let fh_r = hur;
let g = G_ACCEL;
let fhu_l = hl * ul * ul + 0.5 * g * hl * hl;
let fhu_r = hr * ur * ur + 0.5 * g * hr * hr;
new_h[i] = 0.5 * (hl + hr) - 0.5 * dt / self.dx * (fh_r - fh_l);
new_hu[i] = 0.5 * (hul + hur) - 0.5 * dt / self.dx * (fhu_r - fhu_l);
new_hu[i] += dt * new_h[i] * self.gravity_along;
if new_h[i] > 1e-12 {
let u_new = new_hu[i] / new_h[i];
new_hu[i] -= dt * self.friction * u_new;
}
}
new_h[0] = new_h[1];
new_hu[0] = -new_hu[1];
new_h[n - 1] = new_h[n - 2];
new_hu[n - 1] = -new_hu[n - 2];
for i in 0..n {
new_h[i] = new_h[i].max(0.0);
self.cells[i].h = new_h[i];
self.cells[i].u = if new_h[i] > 1e-12 {
new_hu[i] / new_h[i]
} else {
0.0
};
}
}
pub fn cfl_dt(&self, cfl: f64) -> f64 {
let mut max_speed = 1e-15;
for c in &self.cells {
let speed = c.u.abs() + (G_ACCEL * c.h).sqrt();
if speed > max_speed {
max_speed = speed;
}
}
cfl * self.dx / max_speed
}
}
#[derive(Clone, Debug)]
pub struct FilmThicknessEvolution1D {
pub h: Vec<f64>,
pub dx: f64,
pub density: f64,
pub viscosity: f64,
pub surface_tension: f64,
pub inclination: f64,
}
impl FilmThicknessEvolution1D {
pub fn new(
n: usize,
dx: f64,
h0: f64,
density: f64,
viscosity: f64,
surface_tension: f64,
inclination: f64,
) -> Self {
Self {
h: vec![h0; n],
dx,
density,
viscosity,
surface_tension,
inclination,
}
}
pub fn num_points(&self) -> usize {
self.h.len()
}
pub fn total_volume(&self) -> f64 {
self.h.iter().map(|hi| hi * self.dx).sum()
}
pub fn gravity_flux_coeff(&self) -> f64 {
self.density * G_ACCEL * self.inclination.sin() / (3.0 * self.viscosity)
}
pub fn capillary_flux_coeff(&self) -> f64 {
self.surface_tension / (3.0 * self.viscosity)
}
pub fn step(&mut self, dt: f64) {
let n = self.h.len();
if n < 5 {
return;
}
let grav_coeff = self.gravity_flux_coeff();
let cap_coeff = self.capillary_flux_coeff();
let dx = self.dx;
let dx2 = dx * dx;
let dx4 = dx2 * dx2;
let old_h = self.h.clone();
for i in 2..n - 2 {
let hi = old_h[i];
if hi < 1e-15 {
continue;
}
let h3 = hi * hi * hi;
let dh3_dx = (old_h[i + 1].powi(3) - old_h[i - 1].powi(3)) / (2.0 * dx);
let d4h = if i >= 2 && i + 2 < n {
(old_h[i + 2] - 4.0 * old_h[i + 1] + 6.0 * old_h[i] - 4.0 * old_h[i - 1]
+ old_h[i - 2])
/ dx4
} else {
0.0
};
self.h[i] = hi - dt * grav_coeff * dh3_dx - dt * cap_coeff * h3 * d4h;
self.h[i] = self.h[i].max(0.0);
}
}
pub fn stable_dt(&self) -> f64 {
let h_max = self.h.iter().cloned().fold(0.0_f64, f64::max);
if h_max < 1e-15 {
return 1e-6;
}
let cap_coeff = self.capillary_flux_coeff();
let dx4 = self.dx.powi(4);
let h3 = h_max * h_max * h_max;
dx4 / (16.0 * cap_coeff * h3 + 1e-30)
}
}
#[derive(Clone, Debug)]
pub struct MarangoniFlow {
pub dgamma_dt: f64,
pub viscosity: f64,
pub film_thickness: f64,
}
impl MarangoniFlow {
pub fn new(dgamma_dt: f64, viscosity: f64, film_thickness: f64) -> Self {
Self {
dgamma_dt,
viscosity,
film_thickness,
}
}
pub fn marangoni_stress(&self, dt_dx: f64) -> f64 {
self.dgamma_dt * dt_dx
}
pub fn marangoni_number(&self, delta_t: f64, length: f64, thermal_diff: f64) -> f64 {
-(self.dgamma_dt) * delta_t * length / (self.viscosity * thermal_diff)
}
pub fn surface_velocity(&self, dt_dx: f64) -> f64 {
let tau = self.marangoni_stress(dt_dx);
tau * self.film_thickness / (2.0 * self.viscosity)
}
pub fn average_velocity(&self, dt_dx: f64) -> f64 {
let tau = self.marangoni_stress(dt_dx);
tau * self.film_thickness / (3.0 * self.viscosity)
}
pub fn flow_rate(&self, dt_dx: f64) -> f64 {
let tau = self.marangoni_stress(dt_dx);
tau * self.film_thickness * self.film_thickness / (3.0 * self.viscosity)
}
pub fn solutal_stress(dgamma_dc: f64, dc_dx: f64) -> f64 {
dgamma_dc * dc_dx
}
pub fn thermocapillary_velocity(&self, delta_t: f64) -> f64 {
self.dgamma_dt.abs() * delta_t / self.viscosity
}
}
#[derive(Clone, Debug)]
pub struct SurfaceTensionModel {
pub gamma0: f64,
pub dgamma_dt: f64,
pub t_ref: f64,
}
impl SurfaceTensionModel {
pub fn new(gamma0: f64, dgamma_dt: f64, t_ref: f64) -> Self {
Self {
gamma0,
dgamma_dt,
t_ref,
}
}
pub fn water() -> Self {
Self::new(GAMMA_WATER, -1.5e-4, 293.15)
}
pub fn gamma(&self, t: f64) -> f64 {
(self.gamma0 + self.dgamma_dt * (t - self.t_ref)).max(0.0)
}
pub fn gradient(&self) -> f64 {
self.dgamma_dt
}
pub fn young_laplace_sphere(&self, t: f64, radius: f64) -> f64 {
2.0 * self.gamma(t) / radius
}
pub fn young_laplace_cylinder(&self, t: f64, radius: f64) -> f64 {
self.gamma(t) / radius
}
pub fn capillary_pressure(&self, t: f64, curvature: f64) -> f64 {
self.gamma(t) * curvature
}
}
#[derive(Clone, Debug)]
pub struct TannerSpreading {
pub volume: f64,
pub surface_tension: f64,
pub viscosity: f64,
pub r0: f64,
}
impl TannerSpreading {
pub fn new(volume: f64, surface_tension: f64, viscosity: f64, r0: f64) -> Self {
Self {
volume,
surface_tension,
viscosity,
r0,
}
}
pub fn time_scale(&self) -> f64 {
self.viscosity * self.r0 / self.surface_tension
}
pub fn radius(&self, t: f64) -> f64 {
let tau = self.time_scale();
if tau < 1e-30 {
return self.r0;
}
self.r0 * (t / tau).powf(0.1)
}
pub fn contact_angle(&self, t: f64) -> f64 {
let r = self.radius(t);
if r < 1e-15 {
return PI / 2.0;
}
let theta = (4.0 * self.volume / (PI * r * r * r)).powf(1.0 / 3.0);
theta.min(PI / 2.0)
}
pub fn spreading_velocity(&self, t: f64) -> f64 {
let tau = self.time_scale();
if tau < 1e-30 || t < 1e-30 {
return 0.0;
}
0.1 * self.r0 / tau * (t / tau).powf(-0.9)
}
pub fn tanner_velocity(&self, theta: f64, radius: f64, epsilon: f64) -> f64 {
if epsilon < 1e-30 || radius <= epsilon {
return 0.0;
}
self.surface_tension * theta.powi(3) / (6.0 * self.viscosity * (radius / epsilon).ln())
}
}
#[derive(Clone, Debug)]
pub struct ThinFilmRupture {
pub hamaker: f64,
pub surface_tension: f64,
pub viscosity: f64,
pub h0: f64,
}
impl ThinFilmRupture {
pub fn new(hamaker: f64, surface_tension: f64, viscosity: f64, h0: f64) -> Self {
Self {
hamaker,
surface_tension,
viscosity,
h0,
}
}
pub fn disjoining_pressure(&self, h: f64) -> f64 {
if h < 1e-15 {
return 0.0;
}
-self.hamaker / (6.0 * PI * h * h * h)
}
pub fn critical_thickness(&self) -> f64 {
(self.hamaker / (2.0 * PI * self.surface_tension)).sqrt()
}
pub fn rupture_time(&self) -> f64 {
12.0 * PI * PI * self.viscosity * self.surface_tension * self.h0.powi(5)
/ (self.hamaker * self.hamaker)
}
pub fn most_unstable_wavelength(&self) -> f64 {
2.0 * PI * self.h0 * self.h0 * (2.0 * PI * self.surface_tension / self.hamaker).sqrt()
}
pub fn max_growth_rate(&self) -> f64 {
let tau = self.rupture_time();
if tau < 1e-30 {
return 0.0;
}
1.0 / tau
}
pub fn is_stable(&self) -> bool {
self.h0 > self.critical_thickness()
}
pub fn spinodal_parameter(&self, h: f64) -> f64 {
if h < 1e-15 {
return f64::INFINITY;
}
self.hamaker / (2.0 * PI * h.powi(4))
}
}
#[derive(Clone, Debug)]
pub struct LubricationModel {
pub viscosity: f64,
pub gap: f64,
pub length: f64,
pub width: f64,
}
impl LubricationModel {
pub fn new(viscosity: f64, gap: f64, length: f64, width: f64) -> Self {
Self {
viscosity,
gap,
length,
width,
}
}
pub fn poiseuille_flow_rate(&self, dp_dx: f64) -> f64 {
-self.gap.powi(3) / (12.0 * self.viscosity) * dp_dx
}
pub fn couette_flow_rate(&self, surface_velocity: f64) -> f64 {
surface_velocity * self.gap / 2.0
}
pub fn combined_flow_rate(&self, surface_velocity: f64, dp_dx: f64) -> f64 {
self.couette_flow_rate(surface_velocity) + self.poiseuille_flow_rate(dp_dx)
}
pub fn squeeze_film_force(&self, v_approach: f64) -> f64 {
3.0 * self.viscosity * self.width * self.length.powi(3) * v_approach
/ (2.0 * self.gap.powi(3))
}
pub fn squeeze_film_damping(&self) -> f64 {
3.0 * self.viscosity * self.width * self.length.powi(3) / (2.0 * self.gap.powi(3))
}
pub fn slider_bearing_pressure(&self, h1: f64, h2: f64, u_surface: f64) -> f64 {
let h_avg = (h1 + h2) / 2.0;
6.0 * self.viscosity * u_surface * self.length * (h1 - h2) / (h_avg * h_avg * (h1 + h2))
}
pub fn slider_bearing_load(&self, h1: f64, h2: f64, u_surface: f64) -> f64 {
if h2 < 1e-15 {
return 0.0;
}
let k = h1 / h2;
if (k - 1.0).abs() < 1e-10 {
return 0.0; }
let km1 = k - 1.0;
let bracket = (k).ln() - 2.0 * km1 / (k + 1.0);
6.0 * self.viscosity * u_surface * self.length * self.length * bracket
/ (h2 * h2 * km1 * km1)
}
pub fn reynolds_rhs(viscosity: f64, u_surface: f64, dh_dx: f64, dh_dt: f64) -> f64 {
6.0 * viscosity * u_surface * dh_dx + 12.0 * viscosity * dh_dt
}
pub fn sommerfeld_number(
viscosity: f64,
speed_rps: f64,
load_per_area: f64,
clearance_ratio: f64,
) -> f64 {
viscosity * speed_rps / (load_per_area * clearance_ratio * clearance_ratio)
}
}
#[derive(Clone, Debug)]
pub struct RivuletFlow {
pub density: f64,
pub viscosity: f64,
pub surface_tension: f64,
pub inclination: f64,
pub contact_angle: f64,
pub flow_rate: f64,
}
impl RivuletFlow {
pub fn new(
density: f64,
viscosity: f64,
surface_tension: f64,
inclination: f64,
contact_angle: f64,
flow_rate: f64,
) -> Self {
Self {
density,
viscosity,
surface_tension,
inclination,
contact_angle,
flow_rate,
}
}
pub fn capillary_length(&self) -> f64 {
(self.surface_tension / (self.density * G_ACCEL)).sqrt()
}
pub fn max_width(&self) -> f64 {
let lc = self.capillary_length();
2.0 * lc * (self.contact_angle / 2.0).sin()
}
pub fn max_height(&self, width: f64) -> f64 {
width * self.contact_angle.tan() / 4.0
}
pub fn mean_velocity(&self) -> f64 {
let w = self.max_width();
let h = self.max_height(w);
self.density * G_ACCEL * self.inclination.sin() * h * h / (3.0 * self.viscosity)
}
pub fn cross_section_area(&self) -> f64 {
let w = self.max_width();
let h = self.max_height(w);
w * h / 2.0
}
pub fn reynolds_number(&self) -> f64 {
let w = self.max_width();
if w < 1e-15 {
return 0.0;
}
self.density * self.flow_rate / (self.viscosity * w)
}
pub fn meander_wavelength(&self) -> f64 {
let w = self.max_width();
let u = self.mean_velocity();
let we = self.density * u * u * w / self.surface_tension;
2.0 * PI * w * we.sqrt()
}
}
#[derive(Clone, Debug)]
pub struct DipCoating {
pub density: f64,
pub viscosity: f64,
pub surface_tension: f64,
pub velocity: f64,
}
impl DipCoating {
pub fn new(density: f64, viscosity: f64, surface_tension: f64, velocity: f64) -> Self {
Self {
density,
viscosity,
surface_tension,
velocity,
}
}
pub fn film_thickness(&self) -> f64 {
let ca = self.capillary_number();
let lc = (self.surface_tension / (self.density * G_ACCEL)).sqrt();
0.94 * lc * ca.powf(2.0 / 3.0)
}
pub fn capillary_number(&self) -> f64 {
self.viscosity * self.velocity / self.surface_tension
}
pub fn draining_thickness(&self) -> f64 {
(self.viscosity * self.velocity / (self.density * G_ACCEL)).sqrt()
}
pub fn draining_film_thickness(&self, h0: f64, t: f64) -> f64 {
let factor = 1.0 + self.density * G_ACCEL * h0 * h0 * t / (3.0 * self.viscosity);
h0 / factor.sqrt()
}
pub fn thickness_variation(&self, h0: f64, t: f64, x_positions: &[f64]) -> f64 {
if x_positions.is_empty() {
return 0.0;
}
let thicknesses: Vec<f64> = x_positions
.iter()
.map(|&_x| self.draining_film_thickness(h0, t))
.collect();
let h_max = thicknesses.iter().cloned().fold(0.0_f64, f64::max);
let h_min = thicknesses.iter().cloned().fold(f64::INFINITY, f64::min);
if h_max < 1e-15 {
return 0.0;
}
(h_max - h_min) / h_max
}
}
#[derive(Clone, Debug)]
pub struct SpinCoating {
pub density: f64,
pub viscosity: f64,
pub omega: f64,
pub h0: f64,
}
impl SpinCoating {
pub fn new(density: f64, viscosity: f64, omega: f64, h0: f64) -> Self {
Self {
density,
viscosity,
omega,
h0,
}
}
pub fn thickness(&self, t: f64) -> f64 {
let factor = 1.0
+ 4.0 * self.density * self.omega * self.omega * self.h0 * self.h0 * t
/ (3.0 * self.viscosity);
self.h0 / factor.sqrt()
}
pub fn final_thickness(&self, evaporation_rate: f64) -> f64 {
(3.0 * self.viscosity * evaporation_rate / (2.0 * self.density * self.omega * self.omega))
.powf(1.0 / 3.0)
}
pub fn thinning_rate(&self, t: f64) -> f64 {
let h = self.thickness(t);
-2.0 * self.density * self.omega * self.omega * h * h * h / (3.0 * self.viscosity)
}
pub fn edge_bead_ratio(&self, radius: f64) -> f64 {
let _h_bulk = self.thickness(1.0); let ca = self.viscosity * self.omega * radius / 0.072; 1.0 + ca.min(2.0) * 0.5
}
}
#[derive(Clone, Debug)]
pub struct FilmNode {
pub position: [f64; 3],
pub normal: [f64; 3],
pub thickness: f64,
pub velocity: [f64; 3],
pub source_rate: f64,
}
impl FilmNode {
pub fn new(position: [f64; 3], normal: [f64; 3], thickness: f64) -> Self {
Self {
position,
normal: vec_normalize(normal),
thickness,
velocity: [0.0; 3],
source_rate: 0.0,
}
}
pub fn project_tangent(&self, v: [f64; 3]) -> [f64; 3] {
let d = vec_dot(v, self.normal);
vec_sub(v, vec_scale(self.normal, d))
}
pub fn tangent_gravity(&self, gravity: [f64; 3]) -> [f64; 3] {
self.project_tangent(gravity)
}
}
#[derive(Clone, Debug)]
pub struct FluidFilmSimulation {
pub nodes: Vec<FilmNode>,
pub edges: Vec<(usize, usize)>,
pub properties: FluidFilmProperties,
pub gravity: [f64; 3],
pub h_min: f64,
}
impl FluidFilmSimulation {
pub fn new(
nodes: Vec<FilmNode>,
edges: Vec<(usize, usize)>,
properties: FluidFilmProperties,
gravity: [f64; 3],
) -> Self {
Self {
nodes,
edges,
properties,
gravity,
h_min: 1e-10,
}
}
pub fn total_volume(&self) -> f64 {
self.nodes.iter().map(|n| n.thickness).sum()
}
pub fn average_thickness(&self) -> f64 {
let n = self.nodes.len() as f64;
if n < 1.0 {
return 0.0;
}
self.total_volume() / n
}
pub fn max_thickness(&self) -> f64 {
self.nodes
.iter()
.map(|n| n.thickness)
.fold(0.0_f64, f64::max)
}
pub fn step(&mut self, dt: f64) {
let n = self.nodes.len();
let mut dh = vec![0.0_f64; n];
let rho = self.properties.density;
let mu = self.properties.viscosity;
let _gamma = self.properties.surface_tension;
for &(i, j) in &self.edges {
let dx = vec_sub(self.nodes[j].position, self.nodes[i].position);
let dist = vec_len(dx);
if dist < 1e-15 {
continue;
}
let hi = self.nodes[i].thickness;
let hj = self.nodes[j].thickness;
let h_avg = 0.5 * (hi + hj);
let g_tang_i = self.nodes[i].tangent_gravity(self.gravity);
let g_tang_j = self.nodes[j].tangent_gravity(self.gravity);
let g_avg = vec_scale(vec_add(g_tang_i, g_tang_j), 0.5);
let g_along = vec_dot(g_avg, vec_normalize(dx));
let flux_grav = rho * g_along * h_avg.powi(3) / (3.0 * mu);
let dh_dx = (hj - hi) / dist;
let flux_level = rho * G_ACCEL * h_avg.powi(3) * dh_dx / (3.0 * mu);
let total_flux = (flux_grav + flux_level) * dt / dist;
dh[i] += total_flux;
dh[j] -= total_flux;
}
for (i, &dhi) in dh.iter().enumerate().take(n) {
self.nodes[i].thickness += dhi + self.nodes[i].source_rate * dt;
self.nodes[i].thickness = self.nodes[i].thickness.max(self.h_min);
}
}
pub fn uniformity_cv(&self) -> f64 {
let avg = self.average_thickness();
if avg < 1e-15 {
return 0.0;
}
let variance: f64 = self
.nodes
.iter()
.map(|n| {
let d = n.thickness - avg;
d * d
})
.sum::<f64>()
/ self.nodes.len() as f64;
variance.sqrt() / avg
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
#[test]
fn test_water_capillary_length() {
let fp = FluidFilmProperties::water(PI / 6.0);
let lc = fp.capillary_length();
assert!(
(lc - 2.7e-3).abs() < 0.5e-3,
"Water capillary length should be ~2.7 mm, got {:.6e}",
lc
);
}
#[test]
fn test_kinematic_viscosity() {
let fp = FluidFilmProperties::water(0.0);
let nu = fp.kinematic_viscosity();
assert!(
(nu - 1.0e-6).abs() < 0.1e-6,
"Water kinematic viscosity ~1e-6 m²/s, got {:.6e}",
nu
);
}
#[test]
fn test_capillary_number() {
let fp = FluidFilmProperties::water(0.0);
let ca = fp.capillary_number(0.01);
assert!(ca > 0.0 && ca < 1.0, "Ca should be small for slow flow");
}
#[test]
fn test_shallow_water_conservation() {
let mut sw = ShallowWaterSolver1D::new(50, 0.01, 0.001, 0.0, 0.0);
sw.cells[25].h = 0.005;
let v0 = sw.total_volume();
let dt = sw.cfl_dt(0.5);
for _ in 0..10 {
sw.step(dt);
}
let v1 = sw.total_volume();
assert!(
(v1 - v0).abs() / v0 < 0.1,
"Volume change too large: {:.6e} -> {:.6e}",
v0,
v1
);
}
#[test]
fn test_cfl_dt_positive() {
let sw = ShallowWaterSolver1D::new(50, 0.01, 0.001, 0.1, 0.0);
let dt = sw.cfl_dt(0.5);
assert!(dt > 0.0, "CFL dt must be positive");
}
#[test]
fn test_film_evolution_stable_dt() {
let fte =
FilmThicknessEvolution1D::new(100, 0.001, 1e-4, RHO_WATER, MU_WATER, GAMMA_WATER, 0.1);
let dt = fte.stable_dt();
assert!(dt > 0.0, "Stable dt must be positive");
}
#[test]
fn test_film_evolution_flat_conservation() {
let mut fte =
FilmThicknessEvolution1D::new(50, 0.001, 1e-4, RHO_WATER, MU_WATER, GAMMA_WATER, 0.0);
let v0 = fte.total_volume();
let dt = fte.stable_dt().min(1e-6);
for _ in 0..5 {
fte.step(dt);
}
let v1 = fte.total_volume();
assert!(
(v1 - v0).abs() / v0 < 0.01,
"Volume should be conserved for flat film"
);
}
#[test]
fn test_marangoni_stress_linear() {
let mg = MarangoniFlow::new(-1.5e-4, 1e-3, 1e-4);
let tau1 = mg.marangoni_stress(100.0);
let tau2 = mg.marangoni_stress(200.0);
assert!(
(tau2 - 2.0 * tau1).abs() < TOL,
"Marangoni stress should be linear in dT/dx"
);
}
#[test]
fn test_marangoni_number_sign() {
let mg = MarangoniFlow::new(-1.5e-4, 1e-3, 1e-4);
let ma = mg.marangoni_number(10.0, 0.01, 1e-7);
assert!(ma > 0.0, "Ma should be positive, got {:.6}", ma);
}
#[test]
fn test_surface_tension_temperature() {
let stm = SurfaceTensionModel::water();
let g1 = stm.gamma(293.15);
let g2 = stm.gamma(373.15);
assert!(g2 < g1, "Surface tension should decrease with temperature");
}
#[test]
fn test_young_laplace() {
let stm = SurfaceTensionModel::water();
let dp = stm.young_laplace_sphere(293.15, 1e-3);
assert!(dp > 0.0, "Young-Laplace pressure must be positive");
assert!(
(dp - 145.6).abs() < 1.0,
"ΔP should be ~146 Pa, got {:.6}",
dp
);
}
#[test]
fn test_tanner_radius_increases() {
let ts = TannerSpreading::new(1e-9, 0.072, 1e-3, 1e-3);
let r1 = ts.radius(1.0);
let r2 = ts.radius(10.0);
assert!(r2 > r1, "Droplet should spread with time");
}
#[test]
fn test_tanner_velocity_positive() {
let ts = TannerSpreading::new(1e-9, 0.072, 1e-3, 1e-3);
let v = ts.spreading_velocity(1.0);
assert!(v > 0.0, "Spreading velocity must be positive");
}
#[test]
fn test_disjoining_pressure() {
let tfr = ThinFilmRupture::new(1e-20, 0.072, 1e-3, 100e-9);
let pi_h = tfr.disjoining_pressure(100e-9);
assert!(pi_h < 0.0, "Disjoining pressure should be negative for A>0");
}
#[test]
fn test_rupture_time_positive() {
let tfr = ThinFilmRupture::new(1e-20, 0.072, 1e-3, 100e-9);
let tau = tfr.rupture_time();
assert!(tau > 0.0, "Rupture time must be positive");
}
#[test]
fn test_unstable_wavelength() {
let tfr = ThinFilmRupture::new(1e-20, 0.072, 1e-3, 100e-9);
let lam = tfr.most_unstable_wavelength();
assert!(lam > 0.0, "Wavelength must be positive");
}
#[test]
fn test_couette_flow() {
let lub = LubricationModel::new(1e-3, 1e-4, 0.01, 0.01);
let q = lub.couette_flow_rate(1.0);
let expected = 1.0 * 1e-4 / 2.0;
assert!(
(q - expected).abs() < TOL,
"Couette flow mismatch: {:.6e} vs {:.6e}",
q,
expected
);
}
#[test]
fn test_poiseuille_flow_sign() {
let lub = LubricationModel::new(1e-3, 1e-4, 0.01, 0.01);
let q = lub.poiseuille_flow_rate(1e6);
assert!(q < 0.0, "Poiseuille flow opposes positive dP/dx");
}
#[test]
fn test_squeeze_film() {
let lub = LubricationModel::new(1e-3, 1e-4, 0.01, 0.01);
let f = lub.squeeze_film_force(0.001);
assert!(f > 0.0, "Squeeze film force should resist approach");
}
#[test]
fn test_slider_parallel_zero_load() {
let lub = LubricationModel::new(1e-3, 1e-4, 0.01, 0.01);
let load = lub.slider_bearing_load(1e-4, 1e-4, 1.0);
assert!(
load.abs() < TOL,
"Parallel surfaces should give zero load, got {:.6e}",
load
);
}
#[test]
fn test_rivulet_width() {
let r1 = RivuletFlow::new(RHO_WATER, MU_WATER, GAMMA_WATER, 0.5, 0.3, 1e-6);
let r2 = RivuletFlow::new(RHO_WATER, MU_WATER, GAMMA_WATER, 0.5, 0.6, 1e-6);
assert!(
r2.max_width() > r1.max_width(),
"Larger contact angle → wider rivulet"
);
}
#[test]
fn test_dip_coating_thickness() {
let dc = DipCoating::new(RHO_WATER, MU_WATER, GAMMA_WATER, 0.01);
let h = dc.film_thickness();
assert!(h > 0.0, "Film thickness must be positive");
assert!(
h < 1e-3,
"Film thickness should be sub-mm for dip coating, got {:.6e}",
h
);
}
#[test]
fn test_spin_coating_thinning() {
let sc = SpinCoating::new(RHO_WATER, MU_WATER, 200.0 * 2.0 * PI / 60.0, 1e-3);
let h1 = sc.thickness(0.0);
let h2 = sc.thickness(1.0);
let h3 = sc.thickness(10.0);
assert!((h1 - 1e-3).abs() < TOL, "t=0 thickness should be h0");
assert!(h2 < h1, "Film should thin with time");
assert!(h3 < h2, "Film should continue thinning");
}
#[test]
fn test_spin_coating_rate() {
let sc = SpinCoating::new(RHO_WATER, MU_WATER, 200.0 * 2.0 * PI / 60.0, 1e-3);
let rate = sc.thinning_rate(1.0);
assert!(rate < 0.0, "Thinning rate should be negative");
}
#[test]
fn test_film_simulation_positive_thickness() {
let nodes = vec![
FilmNode::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
FilmNode::new([0.01, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
FilmNode::new([0.02, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
];
let edges = vec![(0, 1), (1, 2)];
let props = FluidFilmProperties::water(0.5);
let mut sim = FluidFilmSimulation::new(nodes, edges, props, [0.0, -G_ACCEL, 0.0]);
for _ in 0..10 {
sim.step(1e-4);
}
for (i, n) in sim.nodes.iter().enumerate() {
assert!(
n.thickness > 0.0,
"Node {} thickness must be positive, got {:.6e}",
i,
n.thickness
);
}
}
#[test]
fn test_film_simulation_volume_conservation() {
let nodes = vec![
FilmNode::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 2e-4),
FilmNode::new([0.01, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
FilmNode::new([0.02, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
];
let edges = vec![(0, 1), (1, 2)];
let props = FluidFilmProperties::water(0.5);
let mut sim = FluidFilmSimulation::new(nodes, edges, props, [0.0, 0.0, 0.0]);
let v0 = sim.total_volume();
for _ in 0..100 {
sim.step(1e-5);
}
let v1 = sim.total_volume();
assert!(
(v1 - v0).abs() / v0 < 0.01,
"Volume should be conserved: {:.6e} -> {:.6e}",
v0,
v1
);
}
#[test]
fn test_uniformity_cv_uniform() {
let nodes = vec![
FilmNode::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
FilmNode::new([0.01, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
FilmNode::new([0.02, 0.0, 0.0], [0.0, 1.0, 0.0], 1e-4),
];
let edges = vec![(0, 1), (1, 2)];
let props = FluidFilmProperties::water(0.5);
let sim = FluidFilmSimulation::new(nodes, edges, props, [0.0, 0.0, 0.0]);
let cv = sim.uniformity_cv();
assert!(
cv.abs() < TOL,
"CV should be 0 for uniform film, got {:.6e}",
cv
);
}
#[test]
fn test_bond_number() {
let fp = FluidFilmProperties::water(0.0);
let bo = fp.bond_number(0.001);
assert!(bo > 0.0, "Bond number must be positive");
}
#[test]
fn test_marangoni_velocity_thickness() {
let mg1 = MarangoniFlow::new(-1.5e-4, 1e-3, 1e-4);
let mg2 = MarangoniFlow::new(-1.5e-4, 1e-3, 2e-4);
let v1 = mg1.average_velocity(100.0);
let v2 = mg2.average_velocity(100.0);
assert!(
(v2 / v1 - 2.0).abs() < 1e-6,
"Velocity should scale linearly with h: ratio = {:.6}",
v2 / v1
);
}
#[test]
fn test_thin_film_stability() {
let tfr_thick = ThinFilmRupture::new(1e-20, 0.072, 1e-3, 1e-3);
let tfr_thin = ThinFilmRupture::new(1e-20, 0.072, 1e-3, 1e-12);
assert!(tfr_thick.is_stable(), "Thick film should be stable");
assert!(!tfr_thin.is_stable(), "Very thin film should be unstable");
}
}