use std::collections::BTreeMap;
use std::f64::consts::PI;
use std::sync::{Arc, Mutex, PoisonError};
use super::deposit::{Lines, cell, node_weights, segment_weights};
use super::lattice::LatticeGreen;
pub(super) fn line_kernel(p: [f64; 3], a: [f64; 3], b: [f64; 3], r: f64) -> f64 {
let d: [f64; 3] = core::array::from_fn(|k| b[k] - a[k]);
let len = norm(d);
if len == 0.0 {
return 0.0;
}
let w: [f64; 3] = core::array::from_fn(|k| p[k] - a[k]);
let s = dot(w, d) / len;
let perp2 = (dot(w, w) - s * s).max(0.0);
let rho = (perp2 + r * r).sqrt();
((len - s) / rho).asinh() + (s / rho).asinh()
}
pub(super) fn self_kernel(l: f64, r: f64) -> f64 {
2.0 * (l * (l / r).asinh() - (l * l + r * r).sqrt() + r)
}
pub(super) fn pair_kernel(a: [f64; 3], b: [f64; 3], c: [f64; 3], e: [f64; 3], r: f64) -> f64 {
let len = norm(core::array::from_fn(|k| b[k] - a[k]));
let mut sum = 0.0;
for (x, w) in GAUSS12 {
let tau = 0.5 * (x + 1.0);
let s = tau * tau * (3.0 - 2.0 * tau);
let ds = 6.0 * tau * (1.0 - tau) * 0.5 * w;
let p: [f64; 3] = core::array::from_fn(|k| a[k] + s * (b[k] - a[k]));
sum += ds * line_kernel(p, c, e, r);
}
sum * len
}
const GAUSS12: [(f64, f64); 12] = [
(-0.981_560_634_246_719_3, 0.047_175_336_386_511_83),
(-0.904_117_256_370_474_9, 0.106_939_325_995_318_4),
(-0.769_902_674_194_304_7, 0.160_078_328_543_346_2),
(-0.587_317_954_286_617_4, 0.203_167_426_723_065_9),
(-0.367_831_498_998_180_2, 0.233_492_536_538_354_8),
(-0.125_233_408_511_468_9, 0.249_147_045_813_402_8),
(0.125_233_408_511_468_9, 0.249_147_045_813_402_8),
(0.367_831_498_998_180_2, 0.233_492_536_538_354_8),
(0.587_317_954_286_617_4, 0.203_167_426_723_065_9),
(0.769_902_674_194_304_7, 0.160_078_328_543_346_2),
(0.904_117_256_370_474_9, 0.106_939_325_995_318_4),
(0.981_560_634_246_719_3, 0.047_175_336_386_511_83),
];
pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
pub(super) fn norm(a: [f64; 3]) -> f64 {
dot(a, a).sqrt()
}
pub(super) struct Geometry<'a> {
pub nodes: &'a [[f64; 3]],
pub segs: &'a [(usize, usize)],
pub radius: &'a [f64],
pub component: &'a [usize],
}
impl Geometry<'_> {
fn mid(&self, s: usize) -> [f64; 3] {
let (a, b) = self.segs[s];
core::array::from_fn(|k| 0.5 * (self.nodes[a][k] + self.nodes[b][k]))
}
}
struct Virtual {
lines: [Vec<f64>; 3],
h: [f64; 3],
}
impl Virtual {
fn around(real: Lines<'_>, p: [f64; 3], reach: f64) -> Self {
let c: [usize; 3] = core::array::from_fn(|d| cell(real[d], p[d]));
let h: [f64; 3] = core::array::from_fn(|d| real[d][c[d] + 1] - real[d][c[d]]);
let lines = core::array::from_fn(|d| {
let m = (reach / h[d]).ceil() as i64 + 3;
let x0 = real[d][c[d]];
(-m..=m + 1).map(|i| x0 + i as f64 * h[d]).collect()
});
Self { lines, h }
}
fn lines(&self) -> Lines<'_> {
[&self.lines[0], &self.lines[1], &self.lines[2]]
}
}
static CACHE: Mutex<BTreeMap<[u64; 3], Arc<LatticeGreen>>> = Mutex::new(BTreeMap::new());
const CACHE_SIZE: usize = 256;
struct Greens(BTreeMap<[u64; 3], Arc<LatticeGreen>>);
impl Greens {
fn get(&mut self, h: [f64; 3]) -> Arc<LatticeGreen> {
let key = h.map(f64::to_bits);
self.0
.entry(key)
.or_insert_with(|| {
let cached = CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&key)
.cloned();
cached.unwrap_or_else(|| {
let g = Arc::new(LatticeGreen::new(h, TABLE));
let mut c = CACHE.lock().unwrap_or_else(PoisonError::into_inner);
if c.len() >= CACHE_SIZE {
c.clear();
}
c.insert(key, Arc::clone(&g));
g
})
})
.clone()
}
}
const TABLE: usize = 12;
fn offset(a: [usize; 3], b: [usize; 3]) -> [i64; 3] {
core::array::from_fn(|d| a[d] as i64 - b[d] as i64)
}
pub(super) fn corrections(
real: Lines<'_>,
geo: &Geometry<'_>,
dual: &[f64],
window: f64,
) -> (Vec<f64>, Vec<f64>) {
let mut greens = Greens(BTreeMap::new());
let nn = geo.nodes.len();
let mut halves: Vec<Vec<usize>> = vec![Vec::new(); nn];
for (s, &(a, b)) in geo.segs.iter().enumerate() {
halves[a].push(s);
halves[b].push(s);
}
let local_h = |p: [f64; 3]| -> f64 {
(0..3)
.map(|d| {
let c = cell(real[d], p[d]);
real[d][c + 1] - real[d][c]
})
.fold(0.0, f64::max)
};
let g3 = [
(0.5 - 0.5 * 0.6f64.sqrt(), 5.0 / 18.0),
(0.5, 8.0 / 18.0),
(0.5 + 0.5 * 0.6f64.sqrt(), 5.0 / 18.0),
];
let mut cap_len = vec![0.0; nn];
for j in 0..nn {
let xj = geo.nodes[j];
let reach = window * local_h(xj);
let virt = Virtual::around(real, xj, reach);
let vl = virt.lines();
let g = greens.get(virt.h);
let mut charge: BTreeMap<[usize; 3], f64> = BTreeMap::new();
let mut pieces: Vec<([f64; 3], [f64; 3], f64)> = Vec::new();
for i in 0..nn {
if geo.component[i] != geo.component[j] {
continue;
}
let xi = geo.nodes[i];
if norm(core::array::from_fn(|k| xi[k] - xj[k])) > reach {
continue;
}
for (n, w) in node_weights(vl, xi) {
if w != 0.0 {
*charge.entry(n).or_insert(0.0) += dual[i] * w;
}
}
for &s in &halves[i] {
pieces.push((xi, geo.mid(s), geo.radius[s]));
}
}
let k = |x: [f64; 3]| -> f64 {
let phi: f64 = pieces
.iter()
.map(|&(a, b, r)| line_kernel(x, a, b, r))
.sum::<f64>()
/ (4.0 * PI);
let mut v = 0.0;
for (n, lam) in node_weights(vl, x) {
if lam == 0.0 {
continue;
}
for (m, q) in &charge {
v += lam * q * g.get(offset(n, *m));
}
}
phi - v
};
let mut acc = 0.0;
for &s in &halves[j] {
let m = geo.mid(s);
let half = norm(core::array::from_fn(|d| m[d] - xj[d]));
for (t, w) in g3 {
let x: [f64; 3] = core::array::from_fn(|d| xj[d] + t * (m[d] - xj[d]));
let kv = k(x);
acc += if kv > 0.0 { w * half / kv } else { f64::NAN };
}
}
cap_len[j] = acc;
}
let ns = geo.segs.len();
let mut flux = vec![0.0; ns];
for (s, fl) in flux.iter_mut().enumerate() {
let ms = geo.mid(s);
let reach = window * local_h(ms);
let virt = Virtual::around(real, ms, reach);
let vl = virt.lines();
let g = greens.get(virt.h);
let (a, b) = geo.segs[s];
let (pa, pb) = (geo.nodes[a], geo.nodes[b]);
let ts = dir(pa, pb);
let comp = geo.component[a];
let mut cur: BTreeMap<(usize, [usize; 3]), f64> = BTreeMap::new();
let mut lam = 0.0;
for (s2, &(c, e)) in geo.segs.iter().enumerate() {
if geo.component[c] != comp {
continue;
}
let m2 = geo.mid(s2);
if norm(core::array::from_fn(|k| m2[k] - ms[k])) > reach {
continue;
}
let (pc, pe) = (geo.nodes[c], geo.nodes[e]);
segment_weights(vl, pc, pe, &mut cur);
let cos = dot(ts, dir(pc, pe));
if cos == 0.0 {
continue;
}
let r = geo.radius[s2];
lam += cos
* if s2 == s {
self_kernel(norm(core::array::from_fn(|k| pb[k] - pa[k])), r)
} else {
pair_kernel(pa, pb, pc, pe, r)
};
}
lam /= 4.0 * PI;
let mut own = BTreeMap::new();
segment_weights(vl, pa, pb, &mut own);
let by_comp: [Vec<([usize; 3], f64)>; 3] = core::array::from_fn(|d| {
cur.iter()
.filter(|(k, _)| k.0 == d)
.map(|(k, v)| (k.1, *v))
.collect()
});
let mut lat = 0.0;
for (&(d, n), &w) in &own {
for &(m, jv) in &by_comp[d] {
lat += w * jv * g.get(offset(n, m));
}
}
*fl = lam - lat;
}
(cap_len, flux)
}
fn dir(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
let d: [f64; 3] = core::array::from_fn(|k| b[k] - a[k]);
let l = norm(d);
d.map(|v| v / l)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::wire::lattice_radius;
#[test]
fn kernels() {
let (l, r) = (1.0, 1e-3);
let a = [0.0; 3];
let m = [0.5, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let halves = self_kernel(0.5, r) * 2.0 + 2.0 * pair_kernel(a, m, m, b, r);
assert!(
(halves / self_kernel(l, r) - 1.0).abs() < 1e-4,
"{halves} {}",
self_kernel(l, r)
);
let k = line_kernel([0.0; 3], [-10.0, 0.0, 0.0], [10.0, 0.0, 0.0], 1e-2);
assert!((k - 2.0 * (10.0f64 / 1e-2).asinh()).abs() < 1e-9);
}
fn radii(x0: f64, y0: f64, seg: f64) -> (f64, f64) {
let h = 1.0;
let ls: Vec<f64> = (-40..=40).map(|i| i as f64 * h).collect();
let real: Lines<'_> = [&ls, &ls, &ls];
let n = (40.0 / seg) as usize;
let nodes: Vec<[f64; 3]> = (0..=n).map(|i| [x0, y0, -20.0 + i as f64 * seg]).collect();
let segs: Vec<(usize, usize)> = (0..n).map(|i| (i, i + 1)).collect();
let r0 = 1e-3;
let radius = vec![r0; n];
let component = vec![0; n + 1];
let mut dual = vec![seg; n + 1];
dual[0] = 0.5 * seg;
dual[n] = 0.5 * seg;
let geo = Geometry {
nodes: &nodes,
segs: &segs,
radius: &radius,
component: &component,
};
let (c, l) = corrections(real, &geo, &dual, super::super::WINDOW);
let j = n / 2;
(
r0 * (2.0 * PI * seg / c[j]).exp(),
r0 * (2.0 * PI * l[j] / seg).exp(),
)
}
#[test]
fn axis_aligned_on_node_is_the_lattice_radius() {
let want = lattice_radius(1.0, 1.0);
let (re, rm) = radii(0.0, 0.0, 0.5);
eprintln!("on node: r_e {re:.5}, r_m {rm:.5}, lattice radius {want:.5}");
assert!((re / want - 1.0).abs() < 1e-2);
assert!((rm / want - 1.0).abs() < 1e-2);
let want_c = want * (2.0 * PI * (0.125 + 0.25 / PI)).exp();
let (re, rm) = radii(0.5, 0.5, 0.5);
eprintln!("cell centre: r_e {re:.5}, r_m {rm:.5}, 2-D estimate {want_c:.5}");
assert!((re / want_c - 1.0).abs() < 1e-2);
assert!((rm / want_c - 1.0).abs() < 1e-2);
}
}