use super::gauss;
use super::lines;
#[derive(Clone, Copy, Debug)]
pub(crate) struct AlignedBars {
pub(crate) length: [f64; 2],
pub(crate) width: [f64; 2],
pub(crate) height: [f64; 2],
pub(crate) offset: [f64; 3],
}
impl AlignedBars {
pub(crate) fn same(length: f64, width: f64, height: f64) -> Self {
Self {
length: [length; 2],
width: [width; 2],
height: [height; 2],
offset: [0.0; 3],
}
}
fn axial_start(&self) -> f64 {
self.offset[0] + 0.5 * (self.length[0] - self.length[1])
}
}
const FAR_ORDER: usize = 10;
const CORNER_ORDER: usize = 16;
const SNAP: f64 = 1e-12;
fn overlap_weight(e1: f64, e2: f64, offset: f64, u: f64) -> f64 {
let hi = (0.5 * e1).min(offset + 0.5 * e2 - u);
let lo = (-0.5 * e1).max(offset - 0.5 * e2 - u);
(hi - lo).max(0.0)
}
fn break_points(e1: f64, e2: f64, offset: f64) -> Vec<f64> {
let (sum, diff) = (0.5 * (e1 + e2), 0.5 * (e1 - e2).abs());
let snap = |x: f64| if x.abs() <= SNAP * sum { 0.0 } else { x };
let mut points = vec![
snap(offset - sum),
snap(offset - diff),
snap(offset + diff),
snap(offset + sum),
];
if points[0] < 0.0 && points[3] > 0.0 {
points.push(0.0);
}
points.sort_by(f64::total_cmp);
points.dedup();
points
}
struct Integrator<'a> {
bars: &'a AlignedBars,
axial_start: f64,
axial_scale: f64,
}
impl Integrator<'_> {
fn integrand(&self, u: f64, v: f64) -> f64 {
let b = self.bars;
let weight = overlap_weight(b.width[0], b.width[1], b.offset[1], u)
* overlap_weight(b.height[0], b.height[1], b.offset[2], v);
if weight == 0.0 {
return 0.0;
}
let kernel = lines::parallel(b.length[0], b.length[1], self.axial_start, u.hypot(v));
weight * kernel.unwrap_or(0.0)
}
fn direct(&self, u: [f64; 2], v: [f64; 2]) -> f64 {
let rule = gauss::rule(FAR_ORDER);
let mut sum = 0.0;
for (uu, wu) in rule.on(u[0], u[1]) {
for (vv, wv) in rule.on(v[0], v[1]) {
sum += wu * wv * self.integrand(uu, vv);
}
}
sum
}
fn corner(&self, a: f64, b: f64) -> f64 {
let rule = gauss::rule(CORNER_ORDER);
let mut sum = 0.0;
for (tau, w_tau) in rule.on(0.0, 1.0) {
let s = tau * tau * tau;
let radial = w_tau * 3.0 * tau * tau * s;
for (t, w_t) in rule.on(0.0, 1.0) {
let value = self.integrand(a * s, b * s * t) + self.integrand(a * s * t, b * s);
sum += radial * w_t * value;
}
}
sum * (a * b).abs()
}
fn panel(&self, u: [f64; 2], v: [f64; 2], depth: u32) -> f64 {
let (su, sv) = (u[1] - u[0], v[1] - v[0]);
if su <= 0.0 || sv <= 0.0 {
return 0.0;
}
let gap = |r: [f64; 2]| {
if r[0] > 0.0 {
r[0]
} else if r[1] < 0.0 {
-r[1]
} else {
0.0
}
};
let distance = gap(u).hypot(gap(v));
let size = su.max(sv);
if depth >= 200 {
return self.direct(u, v);
}
let split_u =
|at: f64| self.panel([u[0], at], v, depth + 1) + self.panel([at, u[1]], v, depth + 1);
let split_v =
|at: f64| self.panel(u, [v[0], at], depth + 1) + self.panel(u, [at, v[1]], depth + 1);
if distance > 0.0 {
return if size <= distance {
self.direct(u, v)
} else if su >= sv {
split_u(0.5 * (u[0] + u[1]))
} else {
split_v(0.5 * (v[0] + v[1]))
};
}
let target = su.min(sv).min(self.axial_scale);
let toward = |r: [f64; 2], len: f64| if r[0] == 0.0 { len } else { -len };
if su > 2.0 * target {
return split_u(toward(u, target));
}
if sv > 2.0 * target {
return split_v(toward(v, target));
}
let far = |r: [f64; 2]| if r[0] == 0.0 { r[1] } else { r[0] };
self.corner(far(u), far(v))
}
}
pub(crate) fn integral(bars: &AlignedBars) -> f64 {
let axial_start = bars.axial_start();
let (l1, l2) = (bars.length[0], bars.length[1]);
let separations = [
axial_start + l2,
axial_start,
axial_start + l2 - l1,
axial_start - l1,
];
let largest = separations.iter().fold(0.0_f64, |m, q| m.max(q.abs()));
let axial_scale = separations
.iter()
.map(|q| q.abs())
.filter(|&q| q > 1e-9 * largest)
.fold(f64::INFINITY, f64::min);
let integrator = Integrator {
bars,
axial_start,
axial_scale,
};
let us = break_points(bars.width[0], bars.width[1], bars.offset[1]);
let vs = break_points(bars.height[0], bars.height[1], bars.offset[2]);
let mut sum = 0.0;
for u in us.windows(2) {
for v in vs.windows(2) {
sum += integrator.panel([u[0], u[1]], [v[0], v[1]], 0);
}
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inductance::bar::{bar_integral, corner_differences};
fn closed_form(b: &AlignedBars) -> (f64, f64) {
let result = bar_integral(
&corner_differences(b.length[0], b.length[1], b.offset[0]),
&corner_differences(b.width[0], b.width[1], b.offset[1]),
&corner_differences(b.height[0], b.height[1], b.offset[2]),
);
(result.value, result.relative_error)
}
fn assert_agree(b: &AlignedBars, tolerance: f64) {
let (want, estimate) = closed_form(b);
assert!(estimate < 1e-8, "reference too noisy ({estimate:e}): {b:?}");
let got = integral(b);
let error = ((got - want) / want).abs();
assert!(
error < tolerance + estimate,
"{b:?}: {got} vs {want}, error {error:e}, estimate {estimate:e}"
);
}
#[test]
fn trapezoid_weight_integrates_to_the_product_of_extents() {
for (e1, e2, offset) in [(1.0, 1.0, 0.0), (1.0, 0.3, 0.2), (0.4, 2.0, -3.0)] {
let points = break_points(e1, e2, offset);
let total: f64 = points
.windows(2)
.flat_map(|p| gauss::rule(2).on(p[0], p[1]))
.map(|(u, w)| w * overlap_weight(e1, e2, offset, u))
.sum();
assert!((total - e1 * e2).abs() < 1e-14, "{e1} {e2} {offset}");
}
}
#[test]
fn self_term_matches_the_closed_form() {
for (l, w, h) in [
(1.0, 1.0, 1.0),
(1.0, 0.5, 0.25),
(1.0, 0.1, 0.1),
(1.0, 0.2, 0.02),
(0.05, 1.0, 0.7),
(0.3, 2.0, 0.05),
] {
assert_agree(&AlignedBars::same(l, w, h), 1e-9);
}
}
#[test]
fn touching_overlapping_and_separated_bars_match_the_closed_form() {
let base = AlignedBars::same(1.0, 0.2, 0.1);
let cases = [
[0.0, 0.2, 0.0],
[0.0, 0.0, 0.1],
[0.0, 0.2, 0.1],
[0.3, 0.05, 0.02],
[0.4, 0.5, 0.3],
[1.0, 0.0, 0.0],
[1.5, 0.0, 0.0],
];
for offset in cases {
assert_agree(&AlignedBars { offset, ..base }, 1e-9);
}
}
#[test]
fn unequal_bars_match_the_closed_form() {
let bars = AlignedBars {
length: [1.0, 0.6],
width: [0.3, 0.1],
height: [0.05, 0.2],
offset: [0.1, 0.15, -0.05],
};
assert_agree(&bars, 1e-9);
let nested = AlignedBars {
offset: [0.0, 0.0, 0.0],
..bars
};
assert_agree(&nested, 1e-9);
}
}