mod aligned;
mod bar;
mod batch;
pub mod closed_form;
pub(crate) mod gauss;
mod lines;
mod memo;
pub(crate) mod neumann;
use std::cmp::Ordering;
use thiserror::Error;
use crate::filament::Filament;
use aligned::AlignedBars;
use neumann::Bar;
pub use batch::{
mutual_batch, mutual_batch_detailed, mutual_batch_detailed_with, mutual_batch_with,
partial_inductance_matrix, partial_inductance_matrix_detailed,
partial_inductance_matrix_masked, partial_inductance_matrix_masked_with, Execution,
MutualBatch, PairMask,
};
pub const MU0: f64 = 4.0e-7 * std::f64::consts::PI;
pub(crate) const MU0_OVER_4PI: f64 = 1.0e-7;
const CLOSED_FORM_MAX_ERROR: f64 = 1e-10;
const ORTHOGONAL_COS: f64 = 1e-14;
const CHEAP_POINT_PAIRS: usize = 4096;
#[derive(Clone, Debug, PartialEq, Error)]
pub enum KernelError {
#[error("overlapping parallel filaments {first} and {second} have misaligned cross-sections and coincident sample lines")]
CoincidentSamples {
first: usize,
second: usize,
},
#[error("partial inductance of filaments {first} and {second} is not finite")]
NotFinite {
first: usize,
second: usize,
},
}
impl KernelError {
pub(crate) fn at(self, first: usize, second: usize) -> Self {
match self {
Self::CoincidentSamples { .. } => Self::CoincidentSamples { first, second },
Self::NotFinite { .. } => Self::NotFinite { first, second },
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Method {
Orthogonal,
BarClosedForm,
AlignedQuadrature,
PointQuadrature,
SampledFilaments,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Mutual {
pub value: f64,
pub method: Method,
pub resolved: bool,
}
pub fn self_inductance(filament: &Filament) -> f64 {
let bars = AlignedBars::same(filament.length(), filament.width(), filament.height());
let (integral, _) = aligned_integral(&bars);
MU0_OVER_4PI * (integral * (1.0 / (filament.area() * filament.area())))
}
pub fn mutual_inductance(a: &Filament, b: &Filament) -> Result<f64, KernelError> {
mutual_inductance_detailed(a, b).map(|m| m.value)
}
pub fn mutual_inductance_detailed(a: &Filament, b: &Filament) -> Result<Mutual, KernelError> {
evaluate(a, b, true)
}
fn canonical(a: &Filament, b: &Filament) -> Ordering {
let key = |f: &Filament| {
let (s, e, w) = (f.start(), f.end(), f.width_dir());
[
s.x,
s.y,
s.z,
e.x,
e.y,
e.z,
f.width(),
f.height(),
w.x,
w.y,
w.z,
]
};
let (ka, kb) = (key(a), key(b));
ka.iter()
.zip(&kb)
.map(|(x, y)| x.total_cmp(y))
.find(|o| o.is_ne())
.unwrap_or(Ordering::Equal)
}
fn aligned_integral(bars: &AlignedBars) -> (f64, Method) {
memo::aligned_integral(bars, evaluate_aligned_integral)
}
fn evaluate_aligned_integral(bars: &AlignedBars) -> (f64, Method) {
let exact = bar::bar_integral(
&bar::corner_differences(bars.length[0], bars.length[1], bars.offset[0]),
&bar::corner_differences(bars.width[0], bars.width[1], bars.offset[1]),
&bar::corner_differences(bars.height[0], bars.height[1], bars.offset[2]),
);
if exact.relative_error <= CLOSED_FORM_MAX_ERROR {
(exact.value, Method::BarClosedForm)
} else {
(aligned::integral(bars), Method::AlignedQuadrature)
}
}
fn aligned_bars(a: &Bar, b: &Bar) -> Option<AlignedBars> {
const ALIGNED: f64 = 1.0 - 1e-12;
let (width_b, height_b) = if a.axes[1].dot(&b.axes[1]).abs() > ALIGNED {
(b.half[1], b.half[2])
} else if a.axes[1].dot(&b.axes[2]).abs() > ALIGNED {
(b.half[2], b.half[1])
} else {
return None;
};
let delta = b.centre - a.centre;
Some(AlignedBars {
length: [2.0 * a.half[0], 2.0 * b.half[0]],
width: [2.0 * a.half[1], 2.0 * width_b],
height: [2.0 * a.half[2], 2.0 * height_b],
offset: [
delta.dot(&a.axes[0]),
delta.dot(&a.axes[1]),
delta.dot(&a.axes[2]),
],
})
}
pub(crate) fn evaluate(a: &Filament, b: &Filament, simd: bool) -> Result<Mutual, KernelError> {
let (a, b) = if canonical(a, b) == Ordering::Greater {
(b, a)
} else {
(a, b)
};
let cos = a.direction().dot(&b.direction());
if cos.abs() < ORTHOGONAL_COS {
return Ok(Mutual {
value: 0.0,
method: Method::Orthogonal,
resolved: true,
});
}
let (bar_a, bar_b) = (Bar::new(a), Bar::new(b));
let parallel = a.direction().cross(&b.direction()).norm() < neumann::PARALLEL_SIN;
let aligned = if parallel {
aligned_bars(&bar_a, &bar_b)
} else {
None
};
let gap = neumann::separation(&bar_a, &bar_b);
let orders = neumann::point_orders(&bar_a, gap).zip(neumann::point_orders(&bar_b, gap));
let pairs = |(oa, ob): &([usize; 3], [usize; 3])| {
oa.iter().product::<usize>() * ob.iter().product::<usize>()
};
let point_orders = orders.filter(|o| aligned.is_none() || pairs(o) <= CHEAP_POINT_PAIRS);
let (integral, method, resolved) = if let Some((oa, ob)) = point_orders {
let sum = neumann::point_sum(&bar_a, oa, &bar_b, ob, simd);
(sum * cos, Method::PointQuadrature, true)
} else if let Some(bars) = aligned {
let (integral, method) = aligned_integral(&bars);
let scale = cos.signum() / (a.area() * b.area());
(integral * scale, method, true)
} else {
let sampled = neumann::sampled_filaments(&bar_a, &bar_b, gap, parallel).ok_or(
KernelError::CoincidentSamples {
first: 0,
second: 0,
},
)?;
let direction = if parallel { cos.signum() } else { cos };
(
sampled.value * direction,
Method::SampledFilaments,
sampled.resolved,
)
};
let value = MU0_OVER_4PI * integral;
if !value.is_finite() {
return Err(KernelError::NotFinite {
first: 0,
second: 0,
});
}
Ok(Mutual {
value,
method,
resolved,
})
}