use super::super::{init::domain::Domain, solver::PoissonSolver, types::*};
use rayon::prelude::*;
use realfft::RealFftPlanner;
use rustfft::{FftPlanner, num_complex::Complex};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
type ComplexSlab = (Vec<Complex<f64>>, Vec<Complex<f64>>, Vec<Complex<f64>>);
fn fft_3d_c2c(
buf: &mut [Complex<f64>],
shape: [usize; 3],
plans: &[Arc<dyn rustfft::Fft<f64>>; 3],
) {
let n: usize = shape.iter().product();
let mut scratch = vec![Complex::new(0.0, 0.0); n];
super::fft_utils::fft_3d_c2c_scratch(buf, &mut scratch, shape, plans);
}
pub struct FftPoisson {
pub shape: [usize; 3],
pub dx: [f64; 3],
r2c_z: Arc<dyn realfft::RealToComplex<f64>>,
c2r_z: Arc<dyn realfft::ComplexToReal<f64>>,
fwd: [Arc<dyn rustfft::Fft<f64>>; 3],
inv: [Arc<dyn rustfft::Fft<f64>>; 3],
scratch_cache: std::sync::Mutex<Vec<Complex<f64>>>,
progress: Option<Arc<super::super::progress::StepProgress>>,
}
impl FftPoisson {
pub fn new(domain: &Domain) -> Self {
let shape = [
domain.spatial_res.x1 as usize,
domain.spatial_res.x2 as usize,
domain.spatial_res.x3 as usize,
];
let dx = domain.dx();
let [nx, ny, nz] = shape;
let mut real_planner = RealFftPlanner::<f64>::new();
let r2c_z = real_planner.plan_fft_forward(nz);
let c2r_z = real_planner.plan_fft_inverse(nz);
let mut planner = FftPlanner::new();
let fwd = [
planner.plan_fft_forward(nx),
planner.plan_fft_forward(ny),
planner.plan_fft_forward(nz),
];
let inv = [
planner.plan_fft_inverse(nx),
planner.plan_fft_inverse(ny),
planner.plan_fft_inverse(nz),
];
Self {
shape,
dx,
r2c_z,
c2r_z,
fwd,
inv,
scratch_cache: std::sync::Mutex::new(Vec::new()),
progress: None,
}
}
#[inline]
fn wavenumber(i: usize, n: usize, cell_size: f64) -> f64 {
use std::f64::consts::PI;
let j = if i < n / 2 {
i as i64
} else {
i as i64 - n as i64
};
2.0 * PI * j as f64 / (n as f64 * cell_size)
}
fn fft_3d_real_forward(&self, input: &[f64]) -> Vec<Complex<f64>> {
let _span = tracing::info_span!("fft_forward").entered();
let [nx, ny, nz] = self.shape;
let nz_c = nz / 2 + 1; let n_total_c = nx * ny * nz_c;
let r2c = &self.r2c_z;
let mut buf = vec![Complex::new(0.0, 0.0); n_total_c];
buf.par_chunks_mut(nz_c)
.enumerate()
.for_each(|(row, out_chunk)| {
let start = row * nz;
let mut inbuf = vec![0.0f64; nz];
inbuf.copy_from_slice(&input[start..start + nz]);
if let Err(e) = r2c.process(&mut inbuf, out_chunk) {
tracing::error!("FFT R2C process failed: {e}");
}
});
let mut scratch =
std::mem::take(&mut *self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()));
scratch.resize(n_total_c, Complex::new(0.0, 0.0));
scratch[..n_total_c]
.par_chunks_mut(nz_c * ny)
.enumerate()
.for_each(|(ix, slab)| {
let src = &buf[ix * ny * nz_c..(ix + 1) * ny * nz_c];
super::fft_utils::transpose_tiled(src, slab, ny, nz_c);
});
scratch[..nx * nz_c * ny]
.par_chunks_mut(ny)
.for_each(|row| {
self.fwd[1].process(row);
});
buf.par_chunks_mut(ny * nz_c)
.enumerate()
.for_each(|(ix, slab)| {
let src = &scratch[ix * nz_c * ny..(ix + 1) * nz_c * ny];
super::fft_utils::transpose_tiled(src, slab, nz_c, ny);
});
scratch[..n_total_c]
.par_chunks_mut(nz_c * nx)
.enumerate()
.for_each(|(iy, slab)| {
for iz in 0..nz_c {
for ix in 0..nx {
slab[iz * nx + ix] = buf[ix * ny * nz_c + iy * nz_c + iz];
}
}
});
scratch[..ny * nz_c * nx]
.par_chunks_mut(nx)
.for_each(|row| {
self.fwd[0].process(row);
});
buf.par_chunks_mut(ny * nz_c)
.enumerate()
.for_each(|(ix, slab)| {
for iy in 0..ny {
for iz in 0..nz_c {
slab[iy * nz_c + iz] = scratch[iy * nz_c * nx + iz * nx + ix];
}
}
});
*self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()) = scratch;
buf
}
fn fft_3d_real_inverse(&self, buf: &mut [Complex<f64>]) -> Vec<f64> {
let _span = tracing::info_span!("fft_inverse").entered();
let [nx, ny, nz] = self.shape;
let nz_c = nz / 2 + 1;
let n_total_c = nx * ny * nz_c;
let mut scratch =
std::mem::take(&mut *self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()));
scratch.resize(n_total_c, Complex::new(0.0, 0.0));
scratch[..n_total_c]
.par_chunks_mut(nz_c * nx)
.enumerate()
.for_each(|(iy, slab)| {
for iz in 0..nz_c {
for ix in 0..nx {
slab[iz * nx + ix] = buf[ix * ny * nz_c + iy * nz_c + iz];
}
}
});
scratch[..ny * nz_c * nx]
.par_chunks_mut(nx)
.for_each(|row| {
self.inv[0].process(row);
});
buf.par_chunks_mut(ny * nz_c)
.enumerate()
.for_each(|(ix, slab)| {
for iy in 0..ny {
for iz in 0..nz_c {
slab[iy * nz_c + iz] = scratch[iy * nz_c * nx + iz * nx + ix];
}
}
});
scratch[..n_total_c]
.par_chunks_mut(nz_c * ny)
.enumerate()
.for_each(|(ix, slab)| {
let src = &buf[ix * ny * nz_c..(ix + 1) * ny * nz_c];
super::fft_utils::transpose_tiled(src, slab, ny, nz_c);
});
scratch[..nx * nz_c * ny]
.par_chunks_mut(ny)
.for_each(|row| {
self.inv[1].process(row);
});
buf.par_chunks_mut(ny * nz_c)
.enumerate()
.for_each(|(ix, slab)| {
let src = &scratch[ix * nz_c * ny..(ix + 1) * nz_c * ny];
super::fft_utils::transpose_tiled(src, slab, nz_c, ny);
});
for row in 0..nx * ny {
let base = row * nz_c;
buf[base].im = 0.0;
if nz_c > 1 && nz % 2 == 0 {
buf[base + nz_c - 1].im = 0.0;
}
}
*self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()) = scratch;
let c2r = &self.c2r_z;
let n_total_real = nx * ny * nz;
let mut output = vec![0.0f64; n_total_real];
output
.par_chunks_mut(nz)
.enumerate()
.for_each(|(row, out_chunk)| {
let base = row * nz_c;
let mut inbuf = buf[base..base + nz_c].to_vec();
if let Err(e) = c2r.process(&mut inbuf, out_chunk) {
tracing::error!("FFT C2R process failed: {e}");
}
});
output
}
}
impl PoissonSolver for FftPoisson {
fn set_progress(&mut self, p: std::sync::Arc<super::super::progress::StepProgress>) {
self.progress = Some(p);
}
fn solve(&self, density: &DensityField, g: f64) -> PotentialField {
let _span = tracing::info_span!("fft_poisson_solve").entered();
use std::f64::consts::PI;
let [nx, ny, nz] = self.shape;
let nz_c = nz / 2 + 1;
let mut rho_hat = self.fft_3d_real_forward(&density.data);
{
let _s = tracing::info_span!("green_multiply").entered();
let dx = self.dx;
let total = (nx * ny) as u64;
let counter = AtomicU64::new(0);
let report_interval = (total / 100).max(1);
rho_hat
.par_chunks_mut(nz_c)
.enumerate()
.for_each(|(row, chunk)| {
let ix = row / ny;
let iy = row % ny;
let kx = Self::wavenumber(ix, nx, dx[0]);
let ky = Self::wavenumber(iy, ny, dx[1]);
for (iz, c) in chunk.iter_mut().enumerate() {
let kz = 2.0 * std::f64::consts::PI * iz as f64 / (nz as f64 * dx[2]);
let k2 = kx * kx + ky * ky + kz * kz;
if k2 == 0.0 {
*c = Complex::new(0.0, 0.0);
} else {
*c *= -4.0 * PI * g / k2;
}
}
if let Some(ref p) = self.progress {
let c = counter.fetch_add(1, Ordering::Relaxed);
if c.is_multiple_of(report_interval) {
p.set_intra_progress(c, total);
}
}
});
}
let mut phi = self.fft_3d_real_inverse(&mut rho_hat);
let inv_n = 1.0 / (nx * ny * nz) as f64;
phi.par_iter_mut().for_each(|v| *v *= inv_n);
PotentialField {
data: phi,
shape: density.shape,
}
}
fn compute_acceleration(&self, potential: &PotentialField) -> AccelerationField {
let [nx, ny, nz] = self.shape;
let n_total = nx * ny * nz;
let mut phi_hat: Vec<Complex<f64>> = potential
.data
.iter()
.map(|&p| Complex::new(p, 0.0))
.collect();
let mut scratch =
std::mem::take(&mut *self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()));
scratch.resize(n_total, Complex::new(0.0, 0.0));
super::fft_utils::fft_3d_c2c_scratch(&mut phi_hat, &mut scratch, self.shape, &self.fwd);
let i_unit = Complex::new(0.0f64, 1.0);
let dx = self.dx;
let slab_size = ny * nz;
let mut gx_hat = vec![Complex::new(0.0, 0.0); n_total];
let mut gy_hat = vec![Complex::new(0.0, 0.0); n_total];
let mut gz_hat = vec![Complex::new(0.0, 0.0); n_total];
let total = nx as u64;
let counter = AtomicU64::new(0);
let report_interval = (total / 100).max(1);
gx_hat
.par_chunks_mut(slab_size)
.zip(gy_hat.par_chunks_mut(slab_size))
.zip(gz_hat.par_chunks_mut(slab_size))
.enumerate()
.for_each(|(ix, ((gx_slab, gy_slab), gz_slab))| {
let kx = Self::wavenumber(ix, nx, dx[0]);
let base = ix * slab_size;
for iy in 0..ny {
let ky = Self::wavenumber(iy, ny, dx[1]);
for iz in 0..nz {
let kz = Self::wavenumber(iz, nz, dx[2]);
let p = phi_hat[base + iy * nz + iz];
let local = iy * nz + iz;
gx_slab[local] = -i_unit * kx * p;
gy_slab[local] = -i_unit * ky * p;
gz_slab[local] = -i_unit * kz * p;
}
}
if let Some(ref p) = self.progress {
let c = counter.fetch_add(1, Ordering::Relaxed);
if c.is_multiple_of(report_interval) {
p.set_intra_progress(c, total);
}
}
});
super::fft_utils::fft_3d_c2c_scratch(&mut gx_hat, &mut scratch, self.shape, &self.inv);
super::fft_utils::fft_3d_c2c_scratch(&mut gy_hat, &mut scratch, self.shape, &self.inv);
super::fft_utils::fft_3d_c2c_scratch(&mut gz_hat, &mut scratch, self.shape, &self.inv);
*self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()) = scratch;
let inv_norm = 1.0 / n_total as f64;
AccelerationField {
gx: gx_hat.par_iter().map(|c| c.re * inv_norm).collect(),
gy: gy_hat.par_iter().map(|c| c.re * inv_norm).collect(),
gz: gz_hat.par_iter().map(|c| c.re * inv_norm).collect(),
shape: potential.shape,
}
}
}
#[deprecated(
since = "0.0.11",
note = "use VgfPoisson for isolated BC; FftIsolated will be removed in a future release"
)]
pub struct FftIsolated {
pub shape: [usize; 3],
pub dx: [f64; 3],
green_hat: Vec<Complex<f64>>,
fwd: [Arc<dyn rustfft::Fft<f64>>; 3],
inv: [Arc<dyn rustfft::Fft<f64>>; 3],
scratch_cache: std::sync::Mutex<Vec<Complex<f64>>>,
progress: Option<Arc<super::super::progress::StepProgress>>,
}
#[allow(deprecated)]
impl FftIsolated {
pub fn new(domain: &Domain) -> Self {
use std::f64::consts::PI;
let shape = [
domain.spatial_res.x1 as usize,
domain.spatial_res.x2 as usize,
domain.spatial_res.x3 as usize,
];
let dx = domain.dx();
let [nx, ny, nz] = shape;
let [nx2, ny2, nz2] = [2 * nx, 2 * ny, 2 * nz];
let n2_total = nx2 * ny2 * nz2;
let mut planner = FftPlanner::new();
let fwd = [
planner.plan_fft_forward(nx2),
planner.plan_fft_forward(ny2),
planner.plan_fft_forward(nz2),
];
let inv = [
planner.plan_fft_inverse(nx2),
planner.plan_fft_inverse(ny2),
planner.plan_fft_inverse(nz2),
];
let mut green = vec![Complex::new(0.0, 0.0); n2_total];
green
.par_chunks_mut(nz2)
.enumerate()
.for_each(|(row, chunk)| {
let ix = row / ny2;
let iy = row % ny2;
let rx = if ix <= nx {
ix as f64
} else {
ix as f64 - nx2 as f64
} * dx[0];
let ry = if iy <= ny {
iy as f64
} else {
iy as f64 - ny2 as f64
} * dx[1];
for (iz, c) in chunk.iter_mut().enumerate() {
let rz = if iz <= nz {
iz as f64
} else {
iz as f64 - nz2 as f64
} * dx[2];
let r = (rx * rx + ry * ry + rz * rz).sqrt();
if r > 1e-30 {
*c = Complex::new(-1.0 / (4.0 * PI * r), 0.0);
} else {
let dx_avg = (dx[0] + dx[1] + dx[2]) / 3.0;
*c = Complex::new(-2.38 * dx_avg / (4.0 * PI), 0.0);
}
}
});
fft_3d_c2c(&mut green, [nx2, ny2, nz2], &fwd);
Self {
shape,
dx,
green_hat: green,
fwd,
inv,
scratch_cache: std::sync::Mutex::new(Vec::new()),
progress: None,
}
}
}
#[allow(deprecated)]
impl PoissonSolver for FftIsolated {
fn set_progress(&mut self, p: std::sync::Arc<super::super::progress::StepProgress>) {
self.progress = Some(p);
}
fn solve(&self, density: &DensityField, g: f64) -> PotentialField {
let _span = tracing::info_span!("fft_isolated_solve").entered();
use std::f64::consts::PI;
let [nx, ny, nz] = self.shape;
let [nx2, ny2, nz2] = [2 * nx, 2 * ny, 2 * nz];
let n2_total = nx2 * ny2 * nz2;
let mut rho_pad = vec![Complex::new(0.0, 0.0); n2_total];
rho_pad
.par_chunks_mut(nz2)
.enumerate()
.for_each(|(row, chunk)| {
let ix = row / ny2;
let iy = row % ny2;
if ix < nx && iy < ny {
for (iz, c) in chunk.iter_mut().take(nz).enumerate() {
*c = Complex::new(density.data[ix * ny * nz + iy * nz + iz], 0.0);
}
}
});
let mut scratch =
std::mem::take(&mut *self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()));
scratch.resize(n2_total, Complex::new(0.0, 0.0));
super::fft_utils::fft_3d_c2c_scratch(
&mut rho_pad,
&mut scratch,
[nx2, ny2, nz2],
&self.fwd,
);
let dx3 = self.dx[0] * self.dx[1] * self.dx[2];
let factor = 4.0 * PI * g * dx3;
let total = n2_total as u64;
let counter = AtomicU64::new(0);
let report_interval = (total / 100).max(1);
rho_pad
.par_iter_mut()
.zip(self.green_hat.par_iter())
.for_each(|(rho, green)| {
*rho = factor * green * *rho;
if let Some(ref p) = self.progress {
let c = counter.fetch_add(1, Ordering::Relaxed);
if c.is_multiple_of(report_interval) {
p.set_intra_progress(c, total);
}
}
});
super::fft_utils::fft_3d_c2c_scratch(
&mut rho_pad,
&mut scratch,
[nx2, ny2, nz2],
&self.inv,
);
*self.scratch_cache.lock().unwrap_or_else(|e| e.into_inner()) = scratch;
let norm = n2_total as f64;
let mut phi = vec![0.0f64; nx * ny * nz];
phi.par_chunks_mut(nz).enumerate().for_each(|(row, chunk)| {
let ix = row / ny;
let iy = row % ny;
let base = ix * ny2 * nz2 + iy * nz2;
for iz in 0..nz {
chunk[iz] = rho_pad[base + iz].re / norm;
}
});
PotentialField {
data: phi,
shape: density.shape,
}
}
fn compute_acceleration(&self, potential: &PotentialField) -> AccelerationField {
super::utils::finite_difference_acceleration(potential, &self.dx)
}
}