use super::super::solver::PoissonSolver;
use super::super::types::{AccelerationField, DensityField, PotentialField};
use super::utils::finite_difference_acceleration;
use rayon::prelude::*;
pub struct RangeSeparatedPoisson {
split_radius: f64,
dx: [f64; 3],
shape: [usize; 3],
inner: Box<dyn PoissonSolver + Send + Sync>,
}
impl RangeSeparatedPoisson {
pub fn new(
domain: &super::super::init::domain::Domain,
split_radius: f64,
inner: Box<dyn PoissonSolver + Send + Sync>,
) -> Self {
let shape = [
domain.spatial_res.x1 as usize,
domain.spatial_res.x2 as usize,
domain.spatial_res.x3 as usize,
];
Self {
split_radius,
dx: domain.dx(),
shape,
inner,
}
}
}
impl PoissonSolver for RangeSeparatedPoisson {
fn solve(&self, density: &DensityField, g: f64) -> PotentialField {
let [nx, ny, nz] = self.shape;
let long_range = self.inner.solve(density, g);
let r_s = self.split_radius;
let stencil = [
(r_s / self.dx[0]).ceil() as i32,
(r_s / self.dx[1]).ceil() as i32,
(r_s / self.dx[2]).ceil() as i32,
];
let dx3 = self.dx[0] * self.dx[1] * self.dx[2];
let mut phi = long_range.data;
phi.par_chunks_mut(nz).enumerate().for_each(|(ij, chunk)| {
let ix = ij / ny;
let iy = ij % ny;
for iz in 0..nz {
let mut correction = 0.0_f64;
for di in -stencil[0]..=stencil[0] {
for dj in -stencil[1]..=stencil[1] {
for dk in -stencil[2]..=stencil[2] {
if di == 0 && dj == 0 && dk == 0 {
continue;
}
let jx = ((ix as i32 + di).rem_euclid(nx as i32)) as usize;
let jy = ((iy as i32 + dj).rem_euclid(ny as i32)) as usize;
let jz = ((iz as i32 + dk).rem_euclid(nz as i32)) as usize;
let rx = di as f64 * self.dx[0];
let ry = dj as f64 * self.dx[1];
let rz = dk as f64 * self.dx[2];
let r = (rx * rx + ry * ry + rz * rz).sqrt();
let eta = (-r * r / (r_s * r_s)).exp();
let kernel = eta / (4.0 * std::f64::consts::PI * r);
let rho_j = density.data[jx * ny * nz + jy * nz + jz];
correction += g * rho_j * kernel * dx3;
}
}
}
chunk[iz] += correction;
}
});
PotentialField {
data: phi,
shape: self.shape,
}
}
fn compute_acceleration(&self, potential: &PotentialField) -> AccelerationField {
finite_difference_acceleration(potential, &self.dx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tooling::core::init::domain::{Domain, SpatialBoundType, VelocityBoundType};
use crate::tooling::core::poisson::fft::FftPoisson;
fn test_domain(n: i128, l: f64) -> Domain {
Domain::builder()
.spatial_extent(l)
.velocity_extent(1.0)
.spatial_resolution(n)
.velocity_resolution(n)
.t_final(1.0)
.spatial_bc(SpatialBoundType::Periodic)
.velocity_bc(VelocityBoundType::Open)
.build()
.unwrap()
}
#[test]
fn test_range_separated_construction() {
let domain = test_domain(8, 4.0);
let inner = Box::new(FftPoisson::new(&domain));
let _solver = RangeSeparatedPoisson::new(&domain, 1.0, inner);
}
#[test]
fn test_range_separated_uniform_density() {
let n = 8_usize;
let domain = test_domain(n as i128, 4.0);
let inner = Box::new(FftPoisson::new(&domain));
let solver = RangeSeparatedPoisson::new(&domain, 1.5, inner);
let rho = vec![1.0; n * n * n];
let density = DensityField {
data: rho,
shape: [n, n, n],
};
let pot = solver.solve(&density, 1.0);
let acc = solver.compute_acceleration(&pot);
let max_gx = acc.gx.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
let max_gy = acc.gy.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
let max_gz = acc.gz.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
assert!(
max_gx < 1e-10,
"gx should be ~0 for uniform density, got {max_gx}"
);
assert!(
max_gy < 1e-10,
"gy should be ~0 for uniform density, got {max_gy}"
);
assert!(
max_gz < 1e-10,
"gz should be ~0 for uniform density, got {max_gz}"
);
}
#[test]
fn test_range_separated_vs_fft() {
let n = 16_usize;
let l = 4.0;
let domain = test_domain(n as i128, l);
let dx = domain.dx();
let fft_only = FftPoisson::new(&domain);
let inner = Box::new(FftPoisson::new(&domain));
let range_sep = RangeSeparatedPoisson::new(&domain, 1.0, inner);
let sigma = 1.0_f64;
let mut rho = vec![0.0; n * n * n];
for ix in 0..n {
for iy in 0..n {
for iz in 0..n {
let x = -l + (ix as f64 + 0.5) * dx[0];
let y = -l + (iy as f64 + 0.5) * dx[1];
let z = -l + (iz as f64 + 0.5) * dx[2];
let r2 = x * x + y * y + z * z;
rho[ix * n * n + iy * n + iz] = (-r2 / (2.0 * sigma * sigma)).exp();
}
}
}
let density = DensityField {
data: rho,
shape: [n, n, n],
};
let pot_fft = fft_only.solve(&density, 1.0);
let pot_rs = range_sep.solve(&density, 1.0);
let diff_sq: f64 = pot_fft
.data
.iter()
.zip(pot_rs.data.iter())
.map(|(a, b)| (a - b) * (a - b))
.sum();
let norm_sq: f64 = pot_fft.data.iter().map(|v| v * v).sum();
let rel_err = (diff_sq / norm_sq).sqrt();
assert!(
rel_err < 0.5,
"Relative L2 error between FFT and range-separated should be moderate, got {rel_err}"
);
}
#[test]
fn test_range_separated_point_source() {
let n = 16_usize;
let domain = test_domain(n as i128, 4.0);
let inner = Box::new(FftPoisson::new(&domain));
let solver = RangeSeparatedPoisson::new(&domain, 1.5, inner);
let mut rho = vec![0.0; n * n * n];
let centre = n / 2;
rho[centre * n * n + centre * n + centre] = 1.0;
let density = DensityField {
data: rho,
shape: [n, n, n],
};
let pot = solver.solve(&density, 1.0);
let phi_centre = pot.data[centre * n * n + centre * n + centre];
assert!(phi_centre.is_finite(), "Potential must be finite");
assert!(
phi_centre < 0.0,
"Potential at source should be negative, got {phi_centre}"
);
let phi_neighbour = pot.data[centre * n * n + centre * n + centre + 1];
assert!(
phi_neighbour > phi_centre,
"Potential should decay: phi_neighbour ({phi_neighbour}) > phi_centre ({phi_centre})"
);
}
}