use crate::error::{FFTError, FFTResult};
#[inline]
fn cmul(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]
}
#[inline]
fn cadd(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] + b[0], a[1] + b[1]]
}
#[inline]
fn csub(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] - b[0], a[1] - b[1]]
}
#[inline]
fn cscale(a: [f64; 2], s: f64) -> [f64; 2] {
[a[0] * s, a[1] * s]
}
#[inline]
fn cinv(z: [f64; 2]) -> Option<[f64; 2]> {
let norm2 = z[0] * z[0] + z[1] * z[1];
if norm2 < f64::MIN_POSITIVE {
None
} else {
Some([z[0] / norm2, -z[1] / norm2])
}
}
#[inline]
fn cabs2(z: [f64; 2]) -> f64 {
z[0] * z[0] + z[1] * z[1]
}
fn cpow(z: [f64; 2], n: usize) -> [f64; 2] {
if n == 0 {
return [1.0, 0.0];
}
let mut result = [1.0_f64, 0.0_f64];
let mut base = z;
let mut exp = n;
while exp > 0 {
if exp & 1 == 1 {
result = cmul(result, base);
}
base = cmul(base, base);
exp >>= 1;
}
result
}
fn cln(z: [f64; 2]) -> Option<[f64; 2]> {
let r = cabs2(z).sqrt();
if r < f64::MIN_POSITIVE {
return None;
}
let theta = z[1].atan2(z[0]);
Some([r.ln(), theta])
}
fn binom(n: usize, k: usize) -> f64 {
if k > n {
return 0.0;
}
let k = k.min(n - k);
let mut result = 1.0_f64;
for i in 0..k {
result *= (n - i) as f64;
result /= (i + 1) as f64;
}
result
}
#[derive(Debug, Clone)]
pub struct MultipoleExpansion {
pub center: [f64; 2],
pub coeffs: Vec<[f64; 2]>,
pub order: usize,
}
impl MultipoleExpansion {
pub fn new(center: [f64; 2], order: usize) -> Self {
MultipoleExpansion {
center,
coeffs: vec![[0.0, 0.0]; order + 1],
order,
}
}
pub fn add_source(&mut self, pos: [f64; 2], charge: f64) {
let dz = csub(pos, self.center);
self.coeffs[0][0] += charge;
let mut dz_pow = dz; for k in 1..=self.order {
let contrib = cscale(dz_pow, -charge / k as f64);
self.coeffs[k] = cadd(self.coeffs[k], contrib);
if k < self.order {
dz_pow = cmul(dz_pow, dz);
}
}
}
pub fn translate(&self, new_center: [f64; 2]) -> Self {
let mut result = MultipoleExpansion::new(new_center, self.order);
let z0 = csub(self.center, new_center);
result.coeffs[0] = self.coeffs[0];
let mut z0_pow: Vec<[f64; 2]> = vec![[0.0, 0.0]; self.order + 1];
z0_pow[0] = [1.0, 0.0];
for k in 1..=self.order {
z0_pow[k] = cmul(z0_pow[k - 1], z0);
}
for l in 1..=self.order {
let neg_z0_l = cscale(cpow(cscale(z0, -1.0), l), 1.0 / l as f64);
let mut b_l = cmul(self.coeffs[0], neg_z0_l);
for k in 1..=l {
let c_coeff = binom(l - 1, k - 1);
let term = cscale(cmul(self.coeffs[k], z0_pow[l - k]), c_coeff);
b_l = cadd(b_l, term);
}
result.coeffs[l] = b_l;
}
result
}
pub fn to_local(&self, target_center: [f64; 2], order: usize) -> FFTResult<LocalExpansion> {
let z0 = csub(target_center, self.center);
let z0_inv = cinv(z0).ok_or_else(|| {
FFTError::ValueError("M2L: source and target centers coincide".into())
})?;
let mut local = LocalExpansion::new(target_center, order);
let mut inv_z0_pow: Vec<[f64; 2]> = vec![[0.0, 0.0]; self.order + order + 2];
inv_z0_pow[0] = [1.0, 0.0];
for k in 1..inv_z0_pow.len() {
inv_z0_pow[k] = cmul(inv_z0_pow[k - 1], z0_inv);
}
{
let neg_z0 = cscale(z0, -1.0);
let ln_neg_z0 = cln(neg_z0).ok_or_else(|| {
FFTError::ValueError("M2L: degenerate configuration".into())
})?;
let mut b0 = cmul(self.coeffs[0], ln_neg_z0);
for k in 1..=self.order {
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
let contrib = cscale(cmul(self.coeffs[k], inv_z0_pow[k]), sign);
b0 = cadd(b0, contrib);
}
local.coeffs[0] = b0;
}
for l in 1..=order {
let mut inner = cscale(self.coeffs[0], -1.0 / l as f64);
for k in 1..=self.order {
let c_coeff = binom(l + k - 1, k - 1);
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
let term = cscale(
cmul(self.coeffs[k], inv_z0_pow[k]),
c_coeff * sign,
);
inner = cadd(inner, term);
}
local.coeffs[l] = cmul(inner, inv_z0_pow[l]);
}
Ok(local)
}
pub fn evaluate(&self, x: [f64; 2]) -> f64 {
let dz = csub(x, self.center);
let r2 = cabs2(dz);
if r2 < f64::MIN_POSITIVE {
return 0.0;
}
let ln_r = r2.sqrt().ln();
let arg = dz[1].atan2(dz[0]);
let mut phi = self.coeffs[0][0] * ln_r - self.coeffs[0][1] * arg;
if let Some(inv_dz) = cinv(dz) {
let mut inv_dz_pow = inv_dz; for k in 1..=self.order {
phi += self.coeffs[k][0] * inv_dz_pow[0]
- self.coeffs[k][1] * inv_dz_pow[1];
if k < self.order {
inv_dz_pow = cmul(inv_dz_pow, inv_dz);
}
}
}
phi
}
}
#[derive(Debug, Clone)]
pub struct LocalExpansion {
pub center: [f64; 2],
pub coeffs: Vec<[f64; 2]>,
pub order: usize,
}
impl LocalExpansion {
pub fn new(center: [f64; 2], order: usize) -> Self {
LocalExpansion {
center,
coeffs: vec![[0.0, 0.0]; order + 1],
order,
}
}
pub fn add(&mut self, other: &LocalExpansion) {
for k in 0..=self.order.min(other.order) {
self.coeffs[k] = cadd(self.coeffs[k], other.coeffs[k]);
}
}
pub fn translate(&self, new_center: [f64; 2]) -> Self {
let mut result = LocalExpansion::new(new_center, self.order);
let z0 = csub(new_center, self.center);
let mut z0_pow: Vec<[f64; 2]> = vec![[0.0, 0.0]; self.order + 1];
z0_pow[0] = [1.0, 0.0];
for k in 1..=self.order {
z0_pow[k] = cmul(z0_pow[k - 1], z0);
}
for l in 0..=self.order {
let mut c_l = [0.0_f64, 0.0_f64];
for k in l..=self.order {
let c_coeff = binom(k, l);
let term = cscale(cmul(self.coeffs[k], z0_pow[k - l]), c_coeff);
c_l = cadd(c_l, term);
}
result.coeffs[l] = c_l;
}
result
}
pub fn evaluate(&self, x: [f64; 2]) -> f64 {
let dz = csub(x, self.center);
let mut acc = self.coeffs[self.order];
for k in (0..self.order).rev() {
acc = cadd(cmul(acc, dz), self.coeffs[k]);
}
acc[0] }
pub fn evaluate_gradient(&self, x: [f64; 2]) -> [f64; 2] {
let dz = csub(x, self.center);
if self.order == 0 {
return [0.0, 0.0];
}
let mut acc = cscale(self.coeffs[self.order], self.order as f64);
for k in (1..self.order).rev() {
acc = cadd(cmul(acc, dz), cscale(self.coeffs[k], k as f64));
}
[acc[0], acc[1]]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binom() {
assert!((binom(4, 2) - 6.0).abs() < 1e-12);
assert!((binom(5, 0) - 1.0).abs() < 1e-12);
assert!((binom(5, 5) - 1.0).abs() < 1e-12);
}
#[test]
fn test_multipole_single_source() {
let mut m = MultipoleExpansion::new([0.0, 0.0], 5);
m.add_source([0.1, 0.0], 1.0);
let x = [5.0, 0.0];
let approx = m.evaluate(x);
let exact = ((x[0] - 0.1_f64).powi(2) + x[1].powi(2)).sqrt().ln();
assert!(
(approx - exact).abs() < 1e-4,
"approx={approx:.6} exact={exact:.6}"
);
}
#[test]
fn test_local_expansion_single_source() {
let charge_pos = [10.0, 0.0];
let charge = 1.0;
let mut m = MultipoleExpansion::new(charge_pos, 8);
m.add_source(charge_pos, charge);
let target_center = [0.0, 0.0];
let local = m.to_local(target_center, 8).expect("M2L failed");
let x = [0.2, 0.1];
let approx = local.evaluate(x);
let exact = (((x[0] - charge_pos[0]).powi(2) + (x[1] - charge_pos[1]).powi(2)).sqrt()).ln();
assert!(
(approx - exact).abs() < 1e-4,
"local approx={approx:.6} exact={exact:.6}"
);
}
#[test]
fn test_l2l_translation() {
let charge_pos = [10.0, 0.0];
let mut m = MultipoleExpansion::new(charge_pos, 8);
m.add_source(charge_pos, 1.0);
let local_far = m.to_local([0.0, 0.0], 8).expect("M2L failed");
let local_near = local_far.translate([0.1, 0.05]);
let x = [0.15, 0.08];
let approx_far = local_far.evaluate(x);
let approx_near = local_near.evaluate(x);
assert!(
(approx_far - approx_near).abs() < 1e-5,
"far={approx_far:.8} near={approx_near:.8}"
);
}
#[test]
fn test_gradient() {
let charge_pos = [10.0, 0.0];
let mut m = MultipoleExpansion::new(charge_pos, 10);
m.add_source(charge_pos, 1.0);
let local = m.to_local([0.0, 0.0], 10).expect("M2L failed");
let x = [0.1, 0.0];
let grad = local.evaluate_gradient(x);
let h = 1e-6;
let phi_px = local.evaluate([x[0] + h, x[1]]);
let phi_mx = local.evaluate([x[0] - h, x[1]]);
let numerical_gx = (phi_px - phi_mx) / (2.0 * h);
assert!(
(grad[0] - numerical_gx).abs() < 1e-5,
"grad_x={:.8} numerical={:.8}", grad[0], numerical_gx
);
}
}