use crate::error::{IntegrateError, IntegrateResult};
fn laplacian_2d(f: &[Vec<f64>], dx: f64) -> Vec<Vec<f64>> {
let nx = f.len();
let ny = f[0].len();
let inv_dx2 = 1.0 / (dx * dx);
let mut lap = vec![vec![0.0_f64; ny]; nx];
for x in 0..nx {
let xp = (x + 1) % nx;
let xm = (x + nx - 1) % nx;
for y in 0..ny {
let yp = (y + 1) % ny;
let ym = (y + ny - 1) % ny;
lap[x][y] = (f[xp][y] + f[xm][y] + f[x][yp] + f[x][ym] - 4.0 * f[x][y]) * inv_dx2;
}
}
lap
}
fn laplacian_1d_neumann(f: &[f64], dx: f64) -> Vec<f64> {
let n = f.len();
let inv_dx2 = 1.0 / (dx * dx);
let mut lap = vec![0.0_f64; n];
for i in 0..n {
let fp = if i + 1 < n { f[i + 1] } else { f[i] }; let fm = if i > 0 { f[i - 1] } else { f[i] };
lap[i] = (fp + fm - 2.0 * f[i]) * inv_dx2;
}
lap
}
struct Lcg64 {
state: u64,
}
impl Lcg64 {
fn new(seed: u64) -> Self {
Self { state: seed.wrapping_add(1) }
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn uniform(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn noise(&mut self, amp: f64) -> f64 {
amp * (2.0 * self.uniform() - 1.0)
}
}
pub struct CahnHilliardSolver {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub phi: Vec<Vec<f64>>,
pub mu: Vec<Vec<f64>>,
epsilon: f64,
mobility: f64,
}
impl CahnHilliardSolver {
pub fn new(nx: usize, ny: usize, dx: f64, epsilon: f64, mobility: f64) -> Self {
Self {
nx,
ny,
dx,
phi: vec![vec![0.0_f64; ny]; nx],
mu: vec![vec![0.0_f64; ny]; nx],
epsilon,
mobility,
}
}
pub fn random_init(&mut self, noise_amplitude: f64, seed: u64) {
let mut rng = Lcg64::new(seed);
for x in 0..self.nx {
for y in 0..self.ny {
self.phi[x][y] = rng.noise(noise_amplitude);
}
}
self.mu = self.compute_mu();
}
fn compute_mu(&self) -> Vec<Vec<f64>> {
let lap_phi = laplacian_2d(&self.phi, self.dx);
let eps2 = self.epsilon * self.epsilon;
let mut mu = vec![vec![0.0_f64; self.ny]; self.nx];
for x in 0..self.nx {
for y in 0..self.ny {
let phi = self.phi[x][y];
let df = phi * phi * phi - phi;
mu[x][y] = df - eps2 * lap_phi[x][y];
}
}
mu
}
pub fn step(&mut self, dt: f64) {
self.mu = self.compute_mu();
let lap_mu = laplacian_2d(&self.mu, self.dx);
for x in 0..self.nx {
for y in 0..self.ny {
self.phi[x][y] += dt * self.mobility * lap_mu[x][y];
}
}
}
pub fn run(&mut self, n_steps: usize, dt: f64) {
for _ in 0..n_steps {
self.step(dt);
}
self.mu = self.compute_mu();
}
pub fn free_energy(&self) -> f64 {
let mut energy = 0.0;
let eps2 = self.epsilon * self.epsilon;
let nx = self.nx;
let ny = self.ny;
let inv_2dx = 0.5 / self.dx;
for x in 0..nx {
let xp = (x + 1) % nx;
let xm = (x + nx - 1) % nx;
for y in 0..ny {
let yp = (y + 1) % ny;
let ym = (y + ny - 1) % ny;
let phi = self.phi[x][y];
let bulk = (phi * phi - 1.0) * (phi * phi - 1.0) / 4.0;
let grad_x = (self.phi[xp][y] - self.phi[xm][y]) * inv_2dx;
let grad_y = (self.phi[x][yp] - self.phi[x][ym]) * inv_2dx;
let grad2 = grad_x * grad_x + grad_y * grad_y;
energy += (bulk + eps2 / 2.0 * grad2) * self.dx * self.dx;
}
}
energy
}
pub fn volume_fraction(&self) -> f64 {
let total = self.nx * self.ny;
let positive = self
.phi
.iter()
.flat_map(|row| row.iter())
.filter(|&&v| v > 0.0)
.count();
positive as f64 / total as f64
}
pub fn mean_phi(&self) -> f64 {
let sum: f64 = self.phi.iter().flat_map(|row| row.iter()).sum();
sum / (self.nx * self.ny) as f64
}
pub fn epsilon(&self) -> f64 {
self.epsilon
}
pub fn mobility(&self) -> f64 {
self.mobility
}
}
pub struct AllenCahnSolver {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub phi: Vec<Vec<f64>>,
epsilon: f64,
mobility: f64,
}
impl AllenCahnSolver {
pub fn new(nx: usize, ny: usize, dx: f64, epsilon: f64, mobility: f64) -> Self {
Self {
nx,
ny,
dx,
phi: vec![vec![0.0_f64; ny]; nx],
epsilon,
mobility,
}
}
pub fn circle_init(&mut self, radius: f64) {
let cx = self.nx as f64 / 2.0;
let cy = self.ny as f64 / 2.0;
for x in 0..self.nx {
for y in 0..self.ny {
let r = ((x as f64 - cx).powi(2) + (y as f64 - cy).powi(2)).sqrt();
self.phi[x][y] = -((r - radius) / (self.epsilon * 2.0_f64.sqrt())).tanh();
}
}
}
pub fn random_init(&mut self, seed: u64) {
let mut rng = Lcg64::new(seed);
for x in 0..self.nx {
for y in 0..self.ny {
self.phi[x][y] = rng.noise(1.0);
}
}
}
pub fn step(&mut self, dt: f64) {
let lap_phi = laplacian_2d(&self.phi, self.dx);
let eps2 = self.epsilon * self.epsilon;
for x in 0..self.nx {
for y in 0..self.ny {
let phi = self.phi[x][y];
let df = phi * phi * phi - phi; self.phi[x][y] += dt * self.mobility * (eps2 * lap_phi[x][y] - df);
}
}
}
pub fn run(&mut self, n_steps: usize, dt: f64) {
for _ in 0..n_steps {
self.step(dt);
}
}
pub fn interface_length(&self) -> f64 {
let nx = self.nx;
let ny = self.ny;
let inv_2dx = 0.5 / self.dx;
let mut length = 0.0;
for x in 0..nx {
let xp = (x + 1) % nx;
let xm = (x + nx - 1) % nx;
for y in 0..ny {
let yp = (y + 1) % ny;
let ym = (y + ny - 1) % ny;
let grad_x = (self.phi[xp][y] - self.phi[xm][y]) * inv_2dx;
let grad_y = (self.phi[x][yp] - self.phi[x][ym]) * inv_2dx;
length += 0.5 * self.epsilon * (grad_x * grad_x + grad_y * grad_y) * self.dx * self.dx;
}
}
length
}
pub fn volume_fraction(&self) -> f64 {
let total = self.nx * self.ny;
let pos = self
.phi
.iter()
.flat_map(|r| r.iter())
.filter(|&&v| v > 0.0)
.count();
pos as f64 / total as f64
}
}
pub struct StefanProblem {
pub n: usize,
pub dx: f64,
pub temperature: Vec<f64>,
pub interface_pos: f64,
pub latent_heat: f64,
pub stefan_number: f64,
alpha: f64,
}
impl StefanProblem {
pub fn new(
n: usize,
l: f64,
initial_interface: f64,
latent_heat: f64,
stefan_number: f64,
) -> IntegrateResult<Self> {
if n < 3 {
return Err(IntegrateError::ValueError(
"Stefan: need at least 3 grid points".into(),
));
}
if initial_interface <= 0.0 || initial_interface >= l {
return Err(IntegrateError::ValueError(
"Stefan: initial_interface must be strictly inside (0, l)".into(),
));
}
let dx = l / ((n - 1) as f64);
let interface_idx = initial_interface / dx;
let mut temperature = vec![0.0_f64; n];
for i in 0..n {
let xi = (i as f64) * dx;
if xi <= initial_interface {
temperature[i] = -1.0 * (1.0 - xi / initial_interface);
} else {
temperature[i] = 0.0;
}
}
Ok(Self {
n,
dx,
temperature,
interface_pos: interface_idx,
latent_heat,
stefan_number,
alpha: 1.0,
})
}
pub fn step(&mut self, dt: f64) {
let lap = laplacian_1d_neumann(&self.temperature, self.dx);
let mut t_new = self.temperature.clone();
for i in 1..self.n - 1 {
t_new[i] += dt * self.alpha * lap[i];
}
t_new[0] = -1.0;
t_new[self.n - 1] = 0.0;
self.temperature = t_new;
let s_idx = self.interface_pos as usize;
if s_idx + 1 < self.n && s_idx > 0 {
let grad_solid = (self.temperature[s_idx] - self.temperature[s_idx - 1]) / self.dx;
let grad_liquid = (self.temperature[s_idx + 1] - self.temperature[s_idx]) / self.dx;
let ds_dt = (grad_solid - grad_liquid) / (self.latent_heat / self.alpha);
self.interface_pos += dt * ds_dt / self.dx; self.interface_pos = self.interface_pos.clamp(1.0, (self.n - 2) as f64);
}
}
pub fn run(&mut self, t_final: f64, dt: f64) -> Vec<(f64, f64)> {
let mut history = Vec::new();
let mut t = 0.0;
history.push((t, self.interface_pos * self.dx));
while t < t_final {
let dt_actual = dt.min(t_final - t);
self.step(dt_actual);
t += dt_actual;
history.push((t, self.interface_pos * self.dx));
}
history
}
pub fn interface_position_phys(&self) -> f64 {
self.interface_pos * self.dx
}
pub fn stefan_number(&self) -> f64 {
self.stefan_number
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ch_free_energy_decreases() {
let mut solver = CahnHilliardSolver::new(16, 16, 1.0 / 16.0, 0.05, 1.0);
solver.random_init(0.05, 42);
let e0 = solver.free_energy();
solver.run(100, 1e-4);
let e1 = solver.free_energy();
assert!(e1 <= e0 + 1e-8, "energy increased: e0={:.6e} e1={:.6e}", e0, e1);
}
#[test]
fn test_ch_mass_conservation() {
let mut solver = CahnHilliardSolver::new(16, 16, 1.0 / 16.0, 0.05, 1.0);
solver.random_init(0.02, 123);
let m0 = solver.mean_phi();
solver.run(20, 1e-5);
let m1 = solver.mean_phi();
assert!((m1 - m0).abs() < 1e-8, "mass not conserved: Δ={:.2e}", m1 - m0);
}
#[test]
fn test_ch_volume_fraction_range() {
let solver = CahnHilliardSolver::new(8, 8, 1.0 / 8.0, 0.05, 1.0);
let vf = solver.volume_fraction();
assert!((0.0..=1.0).contains(&vf));
}
#[test]
fn test_ch_initial_energy_finite() {
let mut solver = CahnHilliardSolver::new(8, 8, 1.0 / 8.0, 0.1, 1.0);
solver.random_init(0.1, 7);
assert!(solver.free_energy().is_finite());
}
#[test]
fn test_ac_circle_shrinks() {
let nx = 32;
let dx = 1.0 / 32.0;
let mut solver = AllenCahnSolver::new(nx, nx, dx, 0.04, 1.0);
solver.circle_init(0.3);
let len0 = solver.interface_length();
solver.run(10, 1e-5);
let len1 = solver.interface_length();
assert!(len0 > 0.0, "interface length should be positive");
assert!(len1.is_finite());
}
#[test]
fn test_ac_random_init_bounded() {
let mut solver = AllenCahnSolver::new(8, 8, 0.1, 0.05, 1.0);
solver.random_init(42);
for row in &solver.phi {
for &v in row {
assert!(v.abs() <= 1.0 + 1e-10, "phi out of range: {}", v);
}
}
}
#[test]
fn test_ac_volume_fraction() {
let mut solver = AllenCahnSolver::new(8, 8, 0.1, 0.05, 1.0);
solver.circle_init(2.0);
let vf = solver.volume_fraction();
assert!((0.0..=1.0).contains(&vf));
}
#[test]
fn test_stefan_creates_ok() {
let s = StefanProblem::new(20, 1.0, 0.5, 1.0, 1.0);
assert!(s.is_ok());
}
#[test]
fn test_stefan_invalid_interface() {
let s = StefanProblem::new(20, 1.0, 0.0, 1.0, 1.0);
assert!(s.is_err());
let s = StefanProblem::new(20, 1.0, 1.0, 1.0, 1.0);
assert!(s.is_err());
}
#[test]
fn test_stefan_interface_moves() {
let mut s = StefanProblem::new(50, 1.0, 0.3, 1.0, 1.0).expect("StefanProblem::new should succeed with valid params");
let pos0 = s.interface_position_phys();
let history = s.run(0.01, 1e-4);
assert!(!history.is_empty());
let pos_final = history.last().map(|&(_, p)| p).unwrap_or(pos0);
assert!(pos_final.is_finite());
}
#[test]
fn test_stefan_temperature_boundary() {
let mut s = StefanProblem::new(20, 1.0, 0.5, 1.0, 1.0).expect("StefanProblem::new should succeed with valid params");
s.run(0.001, 1e-4);
assert!((s.temperature[0] + 1.0).abs() < 1e-10);
assert!((s.temperature[s.n - 1]).abs() < 1e-10);
}
}