use core::f64::consts::PI;
use geopotential::linalg::gauss;
use geopotential::special;
use metrology::EPS_0;
const K: f64 = 1.0 / (4.0 * PI * EPS_0);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ring {
pub radius: f64,
pub z: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Segment {
pub a: [f64; 3],
pub b: [f64; 3],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Element {
Ring(Ring),
Segment(Segment),
}
pub fn stack(radius: f64, z0: f64, step: f64, n: usize) -> Vec<Ring> {
(0..n).map(|i| Ring { radius, z: z0 + step * i as f64 }).collect()
}
pub fn polyline(points: &[[f64; 3]], step: f64) -> Vec<Segment> {
points
.windows(2)
.flat_map(|w| {
let n = (dist(w[0], w[1]) / step).ceil().max(1.0) as usize;
let at = move |i: usize| lerp(w[0], w[1], i as f64 / n as f64);
(0..n).map(move |i| Segment { a: at(i), b: at(i + 1) })
})
.collect()
}
pub fn solve(elements: &[Element], wire: f64, potentials: &[f64]) -> Vec<f64> {
assert_eq!(elements.len(), potentials.len(), "one potential per element");
let rows = elements
.iter()
.zip(potentials)
.map(|(ei, &v)| {
let mut row: Vec<f64> = elements.iter().map(|ej| coefficient(ei, ej, wire)).collect();
row.push(v);
row
})
.collect();
gauss(rows)
}
pub fn potential_at(elements: &[Element], charges: &[f64], p: [f64; 3]) -> f64 {
elements.iter().zip(charges).map(|(e, &q)| q * unit_potential(e, p)).sum()
}
pub fn field_at(elements: &[Element], charges: &[f64], p: [f64; 3]) -> [f64; 3] {
elements.iter().zip(charges).fold([0.0; 3], |acc, (e, &q)| {
let f = unit_field(e, p);
[acc[0] + q * f[0], acc[1] + q * f[1], acc[2] + q * f[2]]
})
}
pub fn charges(rings: &[Ring], wire: f64, potentials: &[f64]) -> Vec<f64> {
solve(&as_elements(rings), wire, potentials)
}
pub fn potential(rings: &[Ring], charges: &[f64], rho: f64, z: f64) -> f64 {
potential_at(&as_elements(rings), charges, [rho, 0.0, z])
}
pub fn field(rings: &[Ring], charges: &[f64], rho: f64, z: f64) -> [f64; 2] {
let [e_rho, _, e_z] = field_at(&as_elements(rings), charges, [rho, 0.0, z]);
[e_rho, e_z]
}
fn as_elements(rings: &[Ring]) -> Vec<Element> {
rings.iter().map(|&r| Element::Ring(r)).collect()
}
fn coefficient(ei: &Element, ej: &Element, wire: f64) -> f64 {
match (ei, ej) {
_ if ei == ej => match ei {
Element::Ring(r) => (8.0 * r.radius / wire).ln() / (4.0 * PI * PI * EPS_0 * r.radius),
Element::Segment(s) => {
let l = dist(s.a, s.b);
K / l * 2.0 * (l / (2.0 * wire)).asinh()
}
},
(Element::Ring(r), Element::Ring(_)) => unit_potential(ej, [r.radius, 0.0, r.z]),
(Element::Ring(r), Element::Segment(_)) => {
let n = 16;
(0..n)
.map(|k| {
let t = 2.0 * PI * k as f64 / n as f64;
unit_potential(ej, [r.radius * t.cos(), r.radius * t.sin(), r.z])
})
.sum::<f64>()
/ n as f64
}
(Element::Segment(s), _) => unit_potential(ej, lerp(s.a, s.b, 0.5)),
}
}
fn unit_potential(e: &Element, p: [f64; 3]) -> f64 {
match e {
Element::Ring(r) => ring_potential(1.0, *r, p[0].hypot(p[1]), p[2]),
Element::Segment(s) => {
let (l, r1, r2) = (dist(s.a, s.b), dist(p, s.a), dist(p, s.b));
K / l * ((r1 + r2 + l) / (r1 + r2 - l)).ln()
}
}
}
fn unit_field(e: &Element, p: [f64; 3]) -> [f64; 3] {
match e {
Element::Ring(r) => {
let rho = p[0].hypot(p[1]);
let [e_rho, e_z] = ring_field(1.0, *r, rho, p[2]);
if rho == 0.0 { [0.0, 0.0, e_z] } else { [e_rho * p[0] / rho, e_rho * p[1] / rho, e_z] }
}
Element::Segment(s) => segment_field(*s, p),
}
}
fn segment_field(s: Segment, p: [f64; 3]) -> [f64; 3] {
let l = dist(s.a, s.b);
let u = [(s.b[0] - s.a[0]) / l, (s.b[1] - s.a[1]) / l, (s.b[2] - s.a[2]) / l];
let w = [p[0] - s.a[0], p[1] - s.a[1], p[2] - s.a[2]];
let along = w[0] * u[0] + w[1] * u[1] + w[2] * u[2];
let perp = [w[0] - along * u[0], w[1] - along * u[1], w[2] - along * u[2]];
let d = (perp[0] * perp[0] + perp[1] * perp[1] + perp[2] * perp[2]).sqrt();
let (s1, s2) = (-along, l - along);
let (r1, r2) = (s1.hypot(d), s2.hypot(d));
let par = K / l * (1.0 / r2 - 1.0 / r1);
let across = if d > 0.0 { K / l * (s2 / r2 - s1 / r1) / d / d } else { 0.0 };
[par * u[0] + across * perp[0], par * u[1] + across * perp[1], par * u[2] + across * perp[2]]
}
fn ring_potential(q: f64, ring: Ring, rho: f64, z: f64) -> f64 {
q * K * special::ring(ring.radius, rho, z - ring.z).0
}
fn ring_field(q: f64, ring: Ring, rho: f64, z: f64) -> [f64; 2] {
special::ring(ring.radius, rho, z - ring.z).1.map(|g| -q * K * g)
}
fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
}
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, rel: f64) -> bool {
(a / b - 1.0).abs() < rel
}
fn patent_rig(top: f64, bottom: f64) -> (Vec<Ring>, Vec<f64>) {
let mut rings = stack(0.035, 0.065, 0.01, 12);
rings.extend(stack(0.035, -0.065, -0.01, 12));
let volts: Vec<f64> = (0..24).map(|i| if i < 12 { top } else { bottom }).collect();
let q = charges(&rings, 0.75e-3, &volts);
(rings, q)
}
#[test]
fn one_ring_against_direct_summation() {
let (ring, q, rho, z) = (Ring { radius: 0.04, z: 0.01 }, 1e-9, 0.023, 0.037);
let n = 20_000;
let (mut phi, mut er, mut ez) = (0.0, 0.0, 0.0);
for i in 0..n {
let t = 2.0 * PI * (i as f64 + 0.5) / n as f64;
let (dx, dy, dz) = (rho - ring.radius * t.cos(), -ring.radius * t.sin(), z - ring.z);
let d = (dx * dx + dy * dy + dz * dz).sqrt();
let dq = q / n as f64 / (4.0 * PI * EPS_0);
phi += dq / d;
er += dq * dx / d.powi(3);
ez += dq * dz / d.powi(3);
}
assert!(close(ring_potential(q, ring, rho, z), phi, 1e-12));
let [a, b] = ring_field(q, ring, rho, z);
assert!(close(a, er, 1e-12) && close(b, ez, 1e-12), "{a} {er} {b} {ez}");
let [a, b] = ring_field(q, ring, 0.0, 0.05);
let want = q / (4.0 * PI * EPS_0) * 0.04 / (0.04f64.powi(2) + 0.04f64.powi(2)).powf(1.5);
assert!(a == 0.0 && close(b, want, 1e-14));
let [a, _] = ring_field(q, ring, 50.0, ring.z);
assert!(close(a, q / (4.0 * PI * EPS_0 * 2500.0), 1e-6));
}
#[test]
fn one_segment_against_direct_summation() {
let seg = Segment { a: [0.01, -0.02, 0.03], b: [-0.04, 0.05, -0.01] };
let p = [0.02, 0.01, -0.03];
let (n, l) = (200_000, dist(seg.a, seg.b));
let (mut phi, mut e) = (0.0, [0.0; 3]);
for i in 0..n {
let x = lerp(seg.a, seg.b, (i as f64 + 0.5) / n as f64);
let w = [p[0] - x[0], p[1] - x[1], p[2] - x[2]];
let d = dist(p, x);
phi += K / n as f64 / d;
for k in 0..3 {
e[k] += K / n as f64 * w[k] / d.powi(3);
}
}
let el = Element::Segment(seg);
assert!(close(unit_potential(&el, p), phi, 1e-9), "{} {phi}", unit_potential(&el, p));
let f = unit_field(&el, p);
for k in 0..3 {
assert!(close(f[k], e[k], 1e-8), "{k}: {} {}", f[k], e[k]);
}
assert!(close(unit_potential(&el, [100.0, 0.0, 0.0]), K / 100.0, 1e-3));
let ax = Segment { a: [0.0, 0.0, 0.0], b: [0.0, 0.0, l] };
assert!(close(segment_field(ax, [0.0, 0.0, 2.0 * l])[2], K / (2.0 * l * l), 1e-12));
}
#[test]
fn a_wire_holds_its_potential() {
let segs = polyline(&[[0.0, 0.0, 0.0], [0.0, 0.0, 0.3]], 0.01);
assert_eq!(segs.len(), 30);
let els: Vec<Element> = segs.iter().map(|&s| Element::Segment(s)).collect();
let q = solve(&els, 0.75e-3, &[10e3; 30]);
let v = potential_at(&els, &q, [0.75e-3, 0.0, 0.150]);
assert!(close(v, 10e3, 0.02), "{v}");
assert!(q[0] > q[15] && q[29] > q[15]);
let lambda = q[15] / 0.01;
assert!(close(lambda, 2.0 * PI * EPS_0 * 10e3 / (0.3f64 / 0.75e-3).ln(), 0.15), "{lambda:e}");
}
#[test]
fn the_solved_charges_hold_the_potentials() {
let (rings, q) = patent_rig(50e3, 0.0);
let v = potential(&rings, &q, 0.035 + 0.75e-3, 0.065);
assert!(close(v, 50e3, 0.01), "{v}");
let live: f64 = q[..12].iter().sum();
let grounded: f64 = q[12..].iter().sum();
assert!(close(live, 278.53e-9, 1e-3) && close(grounded, -57.54e-9, 1e-3), "{live:e} {grounded:e}");
assert!(close(0.5 * live * 50e3, 6.963e-3, 1e-3));
}
#[test]
fn the_field_at_the_ball() {
let (rings, q) = patent_rig(50e3, 0.0);
let [_, ez] = field(&rings, &q, 0.0, 0.0);
assert!(close(ez, -2.4991e5, 1e-3), "{ez:e}");
let e2 = |z: f64| {
let [a, b] = field(&rings, &q, 0.0, z);
a * a + b * b
};
let grad = (e2(1e-4) - e2(-1e-4)) / 2e-4;
assert!(close(grad, 1.2753e12, 1e-3), "{grad:e}");
let (rings, q) = patent_rig(25e3, -25e3);
let e2 = |z: f64| {
let [a, b] = field(&rings, &q, 0.0, z);
a * a + b * b
};
assert!(((e2(1e-4) - e2(-1e-4)) / 2e-4).abs() < 1e-6 * grad);
}
}