mod fft;
mod grid;
mod near;
use nalgebra::Vector3;
use rayon::prelude::*;
use thiserror::Error;
use crate::filament::Filament;
use crate::inductance::{KernelError, MU0_OVER_4PI};
use fft::Convolver;
use grid::Grid;
use near::NearField;
pub const MAX_FFT_POINTS: usize = 1 << 26;
const MIN_ROWS_PER_TASK: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GridSpacing {
Auto {
cells_per_filament: f64,
},
Fixed(f64),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PfftParams {
pub grid_spacing: GridSpacing,
pub near_field_radius: f64,
pub interpolation_order: usize,
}
pub const DEFAULT_CELLS_PER_FILAMENT: f64 = 8.0;
pub const DEFAULT_NEAR_FIELD_RADIUS: f64 = 8.0;
pub const DEFAULT_INTERPOLATION_ORDER: usize = 5;
impl Default for PfftParams {
fn default() -> Self {
Self {
grid_spacing: GridSpacing::Auto {
cells_per_filament: DEFAULT_CELLS_PER_FILAMENT,
},
near_field_radius: DEFAULT_NEAR_FIELD_RADIUS,
interpolation_order: DEFAULT_INTERPOLATION_ORDER,
}
}
}
#[derive(Clone, Debug, PartialEq, Error)]
pub enum PfftError {
#[error(transparent)]
Kernel(#[from] KernelError),
#[error("invalid pFFT parameter: {0}")]
InvalidParameter(&'static str),
#[error("pFFT grid of {points} padded points exceeds the limit of {limit}")]
GridTooLarge {
points: usize,
limit: usize,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PfftStats {
pub filaments: usize,
pub grid_spacing: f64,
pub grid_dims: [usize; 3],
pub fft_dims: [usize; 3],
pub projection_entries: usize,
pub near_entries: usize,
pub memory_bytes: usize,
}
#[derive(Debug)]
pub struct PfftOperator {
n: usize,
grid: Option<Grid>,
directions: Vec<[f64; 3]>,
projection_offsets: Vec<usize>,
projection_index: Vec<u32>,
projection_weight: Vec<f64>,
spread_offsets: Vec<usize>,
spread_filament: Vec<u32>,
spread_weight: Vec<f64>,
components: Vec<usize>,
near: NearField,
convolver: Option<Convolver>,
}
impl PfftOperator {
pub fn new(filaments: &[Filament], params: &PfftParams) -> Result<Self, PfftError> {
let order = params.interpolation_order;
if order == 0 {
return Err(PfftError::InvalidParameter(
"interpolation_order must be at least 1",
));
}
let radius_ok =
params.near_field_radius.is_finite() && params.near_field_radius >= (order + 1) as f64;
if !radius_ok {
return Err(PfftError::InvalidParameter(
"near_field_radius must be finite and at least interpolation_order + 1",
));
}
match params.grid_spacing {
GridSpacing::Auto { cells_per_filament }
if !(cells_per_filament > 0.0 && cells_per_filament.is_finite()) =>
{
return Err(PfftError::InvalidParameter(
"cells_per_filament must be positive and finite",
));
}
GridSpacing::Fixed(h) if !(h > 0.0 && h.is_finite()) => {
return Err(PfftError::InvalidParameter(
"grid spacing must be positive and finite",
));
}
_ => {}
}
let n = filaments.len();
if n == 0 {
return Ok(Self::empty());
}
let (lo, hi) = bounding_box(filaments);
let spacing = match params.grid_spacing {
GridSpacing::Fixed(h) => h,
GridSpacing::Auto { cells_per_filament } => {
auto_spacing(hi - lo, cells_per_filament * n as f64)
}
};
let grid = Grid::new(lo, hi, spacing, order + 1);
let padded: usize = grid.dims.iter().map(|&d| fft::padded_size(d)).product();
if padded > MAX_FFT_POINTS || grid.len() > u32::MAX as usize {
return Err(PfftError::GridTooLarge {
points: padded,
limit: MAX_FFT_POINTS,
});
}
let radius = params.near_field_radius * spacing;
let projections = grid::project_all(&grid, filaments, radius);
let pairs = near::near_pairs(filaments, radius);
let near = near::precorrect(filaments, &grid, &projections, &pairs)?;
let mut projection_offsets = Vec::with_capacity(n + 1);
projection_offsets.push(0);
let mut projection_index = Vec::new();
let mut projection_weight = Vec::new();
let mut counts = vec![0usize; grid.len()];
for p in &projections {
for &(g, w) in &p.entries {
projection_index.push(g);
projection_weight.push(w);
counts[g as usize] += 1;
}
projection_offsets.push(projection_index.len());
}
let mut spread_offsets = Vec::with_capacity(grid.len() + 1);
spread_offsets.push(0);
for count in &counts {
spread_offsets.push(spread_offsets.last().unwrap() + count);
}
let mut fill = spread_offsets[..grid.len()].to_vec();
let mut spread_filament = vec![0u32; projection_index.len()];
let mut spread_weight = vec![0.0; projection_index.len()];
for (i, p) in projections.iter().enumerate() {
for &(g, w) in &p.entries {
let slot = &mut fill[g as usize];
spread_filament[*slot] = i as u32;
spread_weight[*slot] = w;
*slot += 1;
}
}
let directions: Vec<[f64; 3]> = filaments
.iter()
.map(|f| {
let d = f.direction();
[d.x, d.y, d.z]
})
.collect();
let components = (0..3)
.filter(|&c| directions.iter().any(|d| d[c] != 0.0))
.collect();
let h = grid.spacing;
let convolver = Convolver::new(grid.dims, |d| near::grid_kernel(d, h));
Ok(Self {
n,
grid: Some(grid),
directions,
projection_offsets,
projection_index,
projection_weight,
spread_offsets,
spread_filament,
spread_weight,
components,
near,
convolver: Some(convolver),
})
}
fn empty() -> Self {
Self {
n: 0,
grid: None,
directions: Vec::new(),
projection_offsets: vec![0],
projection_index: Vec::new(),
projection_weight: Vec::new(),
spread_offsets: vec![0],
spread_filament: Vec::new(),
spread_weight: Vec::new(),
components: Vec::new(),
near: NearField {
offsets: vec![0],
..NearField::default()
},
convolver: None,
}
}
pub fn len(&self) -> usize {
self.n
}
pub fn is_empty(&self) -> bool {
self.n == 0
}
pub fn apply(&self, x: &[f64]) -> Vec<f64> {
assert_eq!(
x.len(),
self.n,
"pFFT operator of dimension {} applied to a vector of length {}",
self.n,
x.len()
);
let mut y = self.near_field(x);
let (Some(grid), Some(convolver)) = (&self.grid, &self.convolver) else {
return y;
};
let sources: Vec<Vec<f64>> = self
.components
.iter()
.map(|&c| self.spread(grid.len(), x, c))
.collect();
let mut potentials: Vec<Vec<f64>> = Vec::with_capacity(sources.len());
for pair in sources.chunks(2) {
let (a, b) = convolver.convolve(&pair[0], pair.get(1).map(Vec::as_slice));
potentials.push(a);
if pair.len() == 2 {
potentials.push(b);
}
}
y.par_iter_mut()
.enumerate()
.with_min_len(MIN_ROWS_PER_TASK)
.for_each(|(i, yi)| {
let range = self.projection_offsets[i]..self.projection_offsets[i + 1];
let mut far = 0.0;
for (&c, phi) in self.components.iter().zip(&potentials) {
let mut sum = 0.0;
for (&g, &w) in self.projection_index[range.clone()]
.iter()
.zip(&self.projection_weight[range.clone()])
{
sum += w * phi[g as usize];
}
far += self.directions[i][c] * sum;
}
*yi += MU0_OVER_4PI * far;
});
y
}
fn spread(&self, points: usize, x: &[f64], c: usize) -> Vec<f64> {
let mut q = vec![0.0; points];
q.par_iter_mut()
.enumerate()
.with_min_len(16 * MIN_ROWS_PER_TASK)
.for_each(|(g, value)| {
let range = self.spread_offsets[g]..self.spread_offsets[g + 1];
*value = self.spread_filament[range.clone()]
.iter()
.zip(&self.spread_weight[range])
.map(|(&i, &w)| {
let i = i as usize;
w * self.directions[i][c] * x[i]
})
.sum();
});
q
}
fn near_field(&self, x: &[f64]) -> Vec<f64> {
let near = &self.near;
(0..self.n)
.into_par_iter()
.with_min_len(MIN_ROWS_PER_TASK)
.map(|i| {
let range = near.offsets[i]..near.offsets[i + 1];
near.columns[range.clone()]
.iter()
.zip(&near.values[range])
.map(|(&j, &c)| c * x[j as usize])
.sum()
})
.collect()
}
pub fn stats(&self) -> PfftStats {
let (grid_spacing, grid_dims, grid_points) = self
.grid
.as_ref()
.map_or((0.0, [0; 3], 0), |g| (g.spacing, g.dims, g.len()));
let (fft_dims, fft_points) = self
.convolver
.as_ref()
.map_or(([0; 3], 0), |c| (c.padded_dims(), c.padded_len()));
let projection_entries = self.projection_index.len();
let near_entries = self.near.columns.len();
let usize_bytes = std::mem::size_of::<usize>();
let memory_bytes = projection_entries * 2 * (4 + 8)
+ (self.n + 1 + grid_points + 1) * usize_bytes
+ near_entries * (4 + 8)
+ (self.n + 1) * usize_bytes
+ self.n * 3 * 8
+ fft_points * 8;
PfftStats {
filaments: self.n,
grid_spacing,
grid_dims,
fft_dims,
projection_entries,
near_entries,
memory_bytes,
}
}
}
fn bounding_box(filaments: &[Filament]) -> (Vector3<f64>, Vector3<f64>) {
let mut lo = Vector3::repeat(f64::INFINITY);
let mut hi = Vector3::repeat(f64::NEG_INFINITY);
for f in filaments {
let reach = 0.5 * (f.width() * f.width_dir().abs() + f.height() * f.height_dir().abs());
for end in [f.start(), f.end()] {
lo = lo.inf(&(end - reach));
hi = hi.sup(&(end + reach));
}
}
(lo, hi)
}
fn auto_spacing(extent: Vector3<f64>, cells: f64) -> f64 {
let largest = extent.max();
let count = |h: f64| extent.iter().map(|&e| (e / h).max(1.0)).product::<f64>();
if cells <= 1.0 || largest <= 0.0 {
return largest.max(f64::MIN_POSITIVE);
}
let (mut small, mut large) = (largest / cells, largest);
for _ in 0..200 {
let mid = (small * large).sqrt();
if count(mid) > cells {
small = mid;
} else {
large = mid;
}
}
large
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_spacing_hits_the_cell_count_for_flat_and_solid_boxes() {
let cube = auto_spacing(Vector3::new(1.0, 1.0, 1.0), 1000.0);
assert!((cube - 0.1).abs() < 1e-9);
let plate = auto_spacing(Vector3::new(1.0, 4.0, 1e-6), 400.0);
assert!((plate - 0.1).abs() < 1e-9);
let line = auto_spacing(Vector3::new(2.0, 1e-5, 1e-5), 100.0);
assert!((line - 0.02).abs() < 1e-9);
assert_eq!(auto_spacing(Vector3::new(1.0, 0.5, 0.2), 0.5), 1.0);
}
}