use nalgebra::Vector3;
fn parallel_primitive(q: f64, rho_sq: f64) -> f64 {
let q = q.abs();
let root = (q * q + rho_sq).sqrt();
if q == 0.0 {
-root
} else {
q * (q + root).ln() - root
}
}
pub(crate) fn axial_overlap(l1: f64, l2: f64, offset: f64) -> f64 {
(l1.min(offset + l2) - offset.max(0.0)).max(0.0)
}
pub(crate) fn parallel(l1: f64, l2: f64, offset: f64, rho: f64) -> Option<f64> {
let rho_sq = rho * rho;
let smooth = parallel_primitive(offset + l2, rho_sq)
- parallel_primitive(offset, rho_sq)
- parallel_primitive(offset + l2 - l1, rho_sq)
+ parallel_primitive(offset - l1, rho_sq);
let overlap = axial_overlap(l1, l2, offset);
if overlap == 0.0 {
Some(smooth)
} else if rho > 0.0 {
Some(smooth - 2.0 * overlap * rho.ln())
} else {
None
}
}
fn skew_log_term(x: f64, big_r: f64, a: f64, residual_sq: f64) -> f64 {
if x == 0.0 {
return 0.0;
}
let argument = if a >= 0.0 {
big_r + a
} else {
residual_sq / (big_r - a)
};
if argument > 0.0 {
x * argument.ln()
} else {
0.0
}
}
fn skew_primitive(s: f64, t: f64, d: f64, cos: f64, sin: f64) -> f64 {
let sin_sq = sin * sin;
let versine = sin_sq / (1.0 + cos.abs());
let (planar_sq, a_s, a_t) = if cos >= 0.0 {
let diff = s - t;
(
diff * diff + 2.0 * s * t * versine,
s * versine - diff,
t * versine + diff,
)
} else {
let sum = s + t;
(
sum * sum - 2.0 * s * t * versine,
sum - s * versine,
sum - t * versine,
)
};
let big_r = (planar_sq + d * d).max(0.0).sqrt();
let logs = skew_log_term(s, big_r, a_s, s * s * sin_sq + d * d)
+ skew_log_term(t, big_r, a_t, t * t * sin_sq + d * d);
if d == 0.0 || big_r == 0.0 {
return logs;
}
logs - (d / sin) * ((d * d * cos + s * t * sin_sq) / (d * big_r * sin)).atan()
}
pub(crate) fn skew(
a0: Vector3<f64>,
a1: Vector3<f64>,
b0: Vector3<f64>,
b1: Vector3<f64>,
sin_min: f64,
) -> Option<f64> {
let (la, lb) = ((a1 - a0).norm(), (b1 - b0).norm());
let (u, v) = ((a1 - a0) / la, (b1 - b0) / lb);
let cos = u.dot(&v);
let sin = u.cross(&v).norm();
if sin.is_nan() || sin < sin_min {
return None;
}
let w0 = a0 - b0;
let (du, dv) = (u.dot(&w0), v.dot(&w0));
let sin_sq = sin * sin;
let s0 = (cos * dv - du) / sin_sq;
let t0 = (dv - cos * du) / sin_sq;
let d = (w0 + u * s0 - v * t0).norm();
let d = if d <= 1e-14 * (la + lb + w0.norm()) {
0.0
} else {
d
};
let (s_lo, s_hi) = (-s0, la - s0);
let (t_lo, t_hi) = (-t0, lb - t0);
Some(
skew_primitive(s_hi, t_hi, d, cos, sin)
- skew_primitive(s_hi, t_lo, d, cos, sin)
- skew_primitive(s_lo, t_hi, d, cos, sin)
+ skew_primitive(s_lo, t_lo, d, cos, sin),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inductance::gauss;
fn brute(
a0: Vector3<f64>,
a1: Vector3<f64>,
b0: Vector3<f64>,
b1: Vector3<f64>,
panels: usize,
) -> f64 {
let rule = gauss::rule(16);
let (la, lb) = ((a1 - a0).norm(), (b1 - b0).norm());
let mut sum = 0.0;
for pa in 0..panels {
for pb in 0..panels {
let (s0, s1) = (pa as f64 / panels as f64, (pa + 1) as f64 / panels as f64);
let (t0, t1) = (pb as f64 / panels as f64, (pb + 1) as f64 / panels as f64);
for (s, ws) in rule.on(s0, s1) {
for (t, wt) in rule.on(t0, t1) {
let r = (a0 + (a1 - a0) * s - b0 - (b1 - b0) * t).norm();
sum += ws * wt / r;
}
}
}
}
sum * la * lb
}
fn v(x: f64, y: f64, z: f64) -> Vector3<f64> {
Vector3::new(x, y, z)
}
#[test]
fn equal_side_by_side_filaments_match_the_textbook_formula() {
for (l, rho) in [(1.0_f64, 0.1_f64), (1.0, 1.0), (1.0, 25.0), (3e-3, 2e-6)] {
let want = 2.0 * (l * (l / rho).asinh() - l.hypot(rho) + rho);
let got = parallel(l, l, 0.0, rho).unwrap();
assert!(((got - want) / want).abs() < 1e-12, "l = {l}, ρ = {rho}");
}
}
#[test]
fn parallel_formula_matches_brute_force_for_offset_unequal_filaments() {
for (l1, l2, offset, rho) in [
(1.0, 0.6, 0.3, 0.2),
(1.0, 2.0, -0.7, 0.5),
(1.0, 0.5, 1.4, 0.3),
(0.8, 1.1, -2.5, 0.25),
] {
let got = parallel(l1, l2, offset, rho).unwrap();
let want = brute(
v(0.0, 0.0, 0.0),
v(l1, 0.0, 0.0),
v(offset, rho, 0.0),
v(offset + l2, rho, 0.0),
8,
);
assert!(
((got - want) / want).abs() < 1e-9,
"({l1}, {l2}, {offset}, {rho}): {got} vs {want}"
);
}
}
#[test]
fn collinear_filaments_are_finite_unless_they_overlap() {
let (l1, l2) = (1.0_f64, 0.4_f64);
let want = l1 * ((l1 + l2) / l1).ln() + l2 * ((l1 + l2) / l2).ln();
let got = parallel(l1, l2, l1, 0.0).unwrap();
assert!((got - want).abs() < 1e-14, "{got} vs {want}");
let g = 0.25_f64;
let xlnx = |q: f64| q * q.ln();
let want = xlnx(l1 + g + l2) - xlnx(g + l2) - xlnx(l1 + g) + xlnx(g);
let got = parallel(l1, l2, l1 + g, 0.0).unwrap();
assert!((got - want).abs() < 1e-14, "{got} vs {want}");
assert!(parallel(l1, l2, 0.5, 0.0).is_none());
assert!(parallel(l1, l1, 0.0, 0.0).is_none());
}
#[test]
fn skew_formula_matches_brute_force() {
let cases = [
(
v(0.0, 0.0, 0.0),
v(1.0, 0.2, -0.1),
v(0.3, 0.5, 0.4),
v(0.9, 1.4, 0.2),
),
(
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(1.5, 0.5, 0.0),
v(2.5, 1.7, 0.0),
),
(
v(-1.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(-0.5, -0.8, 0.3),
v(0.4, 0.9, 0.3),
),
(
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(1.2, 0.4, 0.1),
v(0.1, 0.45, 0.12),
),
];
for (a0, a1, b0, b1) in cases {
let got = skew(a0, a1, b0, b1, 1e-6).unwrap();
let want = brute(a0, a1, b0, b1, 16);
assert!(
((got - want) / want).abs() < 1e-9,
"{a0:?} {a1:?} {b0:?} {b1:?}: {got} vs {want}"
);
}
}
#[test]
fn skew_formula_stays_accurate_for_nearly_parallel_close_filaments() {
for angle in [1e-2_f64, 1e-3, 1e-4, 1e-5, 2e-6] {
for sign in [1.0, -1.0] {
let (s, c) = angle.sin_cos();
let (a0, a1) = (v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0));
let centre = v(0.45, 0.05, 0.002);
let half = v(0.4 * c, 0.4 * s, 0.0) * sign;
let (b0, b1) = (centre - half, centre + half);
let got = skew(a0, a1, b0, b1, 1e-6).unwrap();
let want = brute(a0, a1, b0, b1, 16);
assert!(
((got - want) / want).abs() < 1e-8 + 2e-17 / (angle * angle),
"angle {angle}, sign {sign}: {got} vs {want}"
);
}
}
}
#[test]
fn filaments_meeting_at_a_point_match_grovers_formula() {
for angle in [0.3_f64, 0.8, 1.5, 2.6] {
let (l, m) = (1.0_f64, 0.7_f64);
let b1 = v(m * angle.cos(), m * angle.sin(), 0.0);
let big_r = b1.metric_distance(&v(l, 0.0, 0.0));
let want = 2.0 * (l * (m / (l + big_r)).atanh() + m * (l / (m + big_r)).atanh());
let got = skew(v(0.0, 0.0, 0.0), v(l, 0.0, 0.0), v(0.0, 0.0, 0.0), b1, 1e-6).unwrap();
assert!((got - want).abs() < 1e-13, "ε = {angle}: {got} vs {want}");
}
}
#[test]
fn crossing_filaments_are_finite_and_parallel_ones_are_rejected() {
let got = skew(
v(-1.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(-0.5, -0.5, 0.0),
v(0.5, 0.5, 0.0),
1e-6,
)
.unwrap();
assert!(got.is_finite() && got > 0.0);
assert!(skew(
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(0.0, 1.0, 0.0),
v(1.0, 1.0, 0.0),
1e-6
)
.is_none());
}
}