mod correction;
mod deposit;
mod lattice;
use std::collections::BTreeMap;
use super::edge_material::EdgeMaterials;
use super::fields::Fields;
use super::grid::Grid;
use super::operator::{Hooks, Operator, StepContext};
use crate::axis::Axis;
use crate::constants::{EPS0, MU0};
use crate::post::Complex64;
use crate::waveform::Waveform;
use crate::{Error, Result};
use correction::{Geometry, norm};
const WINDOW: f64 = 5.0;
#[derive(Debug, Clone)]
pub struct WireFeed {
segment: usize,
resistance: f64,
source: Option<(Waveform, f64)>,
}
#[derive(Debug, Clone)]
pub struct ObliqueWire {
points: Vec<[f64; 3]>,
radius: f64,
feed: Option<WireFeed>,
max_segment: Option<f64>,
}
impl ObliqueWire {
pub fn new(points: impl IntoIterator<Item = [f64; 3]>, radius: f64) -> Self {
Self {
points: points.into_iter().collect(),
radius,
feed: None,
max_segment: None,
}
}
pub fn feed(mut self, segment: usize, resistance: f64) -> Self {
self.feed = Some(WireFeed {
segment,
resistance,
source: None,
});
self
}
pub fn excite(mut self, waveform: Waveform, amplitude: f64) -> Self {
let f = self.feed.as_mut().expect("excite needs a feed");
f.source = Some((waveform, amplitude));
self
}
pub fn max_segment(mut self, length: f64) -> Self {
self.max_segment = Some(length);
self
}
pub fn points(&self) -> &[[f64; 3]] {
&self.points
}
pub fn radius(&self) -> f64 {
self.radius
}
fn validate(&self) -> Result<()> {
if self.points.len() < 2 || self.points.iter().flatten().any(|v| !v.is_finite()) {
return Err(Error::Invalid(
"oblique wire needs at least two finite points".into(),
));
}
if !(self.radius.is_finite() && self.radius > 0.0) {
return Err(Error::Invalid(format!(
"oblique wire radius {} must be finite and positive",
self.radius
)));
}
if let Some(f) = &self.feed {
if f.segment + 1 >= self.points.len() {
return Err(Error::Invalid(format!(
"wire feed segment {} out of range",
f.segment
)));
}
if !(f.resistance.is_finite() && f.resistance >= 0.0) {
return Err(Error::Invalid(format!(
"wire feed resistance {} must be finite and non-negative",
f.resistance
)));
}
if let Some((w, a)) = &f.source {
w.validate()?;
if !a.is_finite() {
return Err(Error::Invalid("wire feed amplitude is not finite".into()));
}
}
}
if let Some(l) = self.max_segment
&& !(l.is_finite() && l > 0.0)
{
return Err(Error::Invalid(format!(
"wire max segment length {l} must be finite and positive"
)));
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct FeedState {
segment: usize,
resistance: f64,
source: Option<(Waveform, f64)>,
voltage: Vec<f64>,
current: Vec<f64>,
}
#[derive(Debug, Clone)]
struct Envelope {
first: Vec<usize>,
start: Vec<usize>,
data: Vec<f64>,
}
impl Envelope {
fn factor(n: usize, lower: &BTreeMap<(usize, usize), f64>) -> Result<Self> {
let mut first: Vec<usize> = (0..n).collect();
for &(i, j) in lower.keys() {
first[i] = first[i].min(j);
}
let mut start = Vec::with_capacity(n + 1);
let mut len = 0;
for (i, &f) in first.iter().enumerate() {
start.push(len);
len += i - f + 1;
}
let mut data = vec![0.0; len];
for (&(i, j), &v) in lower {
data[start[i] + j - first[i]] += v;
}
let mut m = Self { first, start, data };
for i in 0..n {
for j in m.first[i]..=i {
let k0 = m.first[i].max(m.first[j]);
let mut s = m.at(i, j);
for k in k0..j {
s -= m.at(i, k) * m.at(j, k);
}
if j < i {
let d = m.at(j, j);
m.set(i, j, s / d);
} else {
if !(s > 0.0 && s.is_finite()) {
return Err(Error::Invalid(
"oblique wire: the coupled wire system is not positive definite".into(),
));
}
m.set(i, i, s.sqrt());
}
}
}
Ok(m)
}
fn at(&self, i: usize, j: usize) -> f64 {
self.data[self.start[i] + j - self.first[i]]
}
fn set(&mut self, i: usize, j: usize, v: f64) {
let k = self.start[i] + j - self.first[i];
self.data[k] = v;
}
#[allow(clippy::needless_range_loop)] fn solve(&self, x: &mut [f64]) {
let n = x.len();
for i in 0..n {
let mut s = x[i];
for k in self.first[i]..i {
s -= self.at(i, k) * x[k];
}
x[i] = s / self.at(i, i);
}
for i in (0..n).rev() {
x[i] /= self.at(i, i);
let xi = x[i];
for k in self.first[i]..i {
x[k] -= self.at(i, k) * xi;
}
}
}
}
#[derive(Debug, Clone)]
struct Solver {
dt: f64,
cb: Vec<f64>,
chol: Envelope,
}
#[derive(Debug, Clone)]
pub struct ObliqueWires {
nodes: Vec<[f64; 3]>,
segs: Vec<(usize, usize)>,
seg_wire: Vec<usize>,
l: Vec<f64>,
r: Vec<f64>,
c_inv: Vec<f64>,
connected: Vec<bool>,
edges: Vec<(Axis, [usize; 3])>,
p: Vec<Vec<(usize, f64)>>,
pt: Vec<Vec<(usize, f64)>>,
feeds: Vec<FeedState>,
current: Vec<f64>,
potential: Vec<f64>,
u_old: Vec<f64>,
ustar: Vec<f64>,
avg: Vec<f64>,
src: Vec<f64>,
solver: Option<Solver>,
first_step: Option<u64>,
dt: f64,
}
impl ObliqueWires {
pub fn new(
grid: &Grid,
materials: &EdgeMaterials,
wires: &[ObliqueWire],
contact: impl Fn(Axis, [usize; 3]) -> bool,
) -> Result<Self> {
if grid.is_cylindrical() {
return Err(Error::Invalid(
"oblique thin wires need a Cartesian grid".into(),
));
}
if materials.nodes() != grid.nodes() {
return Err(Error::Invalid(
"oblique wires: material map does not match the grid".into(),
));
}
let lines: [&[f64]; 3] = [
grid.lines(Axis::X),
grid.lines(Axis::Y),
grid.lines(Axis::Z),
];
let extent = (0..3)
.map(|d| lines[d][lines[d].len() - 1] - lines[d][0])
.fold(0.0, f64::max);
let tol = 1e-9 * extent;
for (wi, w) in wires.iter().enumerate() {
w.validate()?;
for p in &w.points {
for d in 0..3 {
let l = lines[d];
if p[d] < l[0] - tol || p[d] > l[l.len() - 1] + tol {
return Err(Error::Invalid(format!(
"oblique wire #{wi}: point {p:?} is outside the grid"
)));
}
}
}
}
let mut nodes: Vec<[f64; 3]> = Vec::new();
let mut vertex_nodes: Vec<usize> = Vec::new();
let mut vertex = |p: [f64; 3], nodes: &mut Vec<[f64; 3]>| -> usize {
for &k in &vertex_nodes {
let q = nodes[k];
if norm(core::array::from_fn(|d| q[d] - p[d])) <= tol {
return k;
}
}
nodes.push(p);
vertex_nodes.push(nodes.len() - 1);
nodes.len() - 1
};
let mut segs = Vec::new();
let mut seg_wire = Vec::new();
let mut radius = Vec::new();
let mut r = Vec::new();
let mut feeds = Vec::new();
for (wi, w) in wires.iter().enumerate() {
let mut prev = vertex(w.points[0], &mut nodes);
for k in 0..w.points.len() - 1 {
let (a, b) = (w.points[k], w.points[k + 1]);
let len = norm(core::array::from_fn(|d| b[d] - a[d]));
if len <= tol {
continue;
}
let feed = w.feed.as_ref().filter(|f| f.segment == k);
let n = if feed.is_some() {
1
} else {
let lmax = w.max_segment.unwrap_or_else(|| {
let mid: [f64; 3] = core::array::from_fn(|d| 0.5 * (a[d] + b[d]));
0.5 * [a, mid, b]
.iter()
.flat_map(|p| {
(0..3).map(|d| {
let c = deposit::cell(lines[d], p[d]);
lines[d][c + 1] - lines[d][c]
})
})
.fold(f64::INFINITY, f64::min)
});
((len / lmax).ceil() as usize).max(1)
};
for i in 1..=n {
let next = if i == n {
vertex(b, &mut nodes)
} else {
let t = i as f64 / n as f64;
nodes.push(core::array::from_fn(|d| a[d] + t * (b[d] - a[d])));
nodes.len() - 1
};
if let Some(f) = feed {
feeds.push(FeedState {
segment: segs.len(),
resistance: f.resistance,
source: f.source.clone(),
voltage: Vec::new(),
current: Vec::new(),
});
r.push(f.resistance);
} else {
r.push(0.0);
}
segs.push((prev, next));
seg_wire.push(wi);
radius.push(w.radius);
prev = next;
}
}
}
if segs.is_empty() {
return Err(Error::Invalid("oblique wires: no segment".into()));
}
let nn = nodes.len();
let mut parent: Vec<usize> = (0..nn).collect();
fn root(p: &mut [usize], mut i: usize) -> usize {
while p[i] != i {
p[i] = p[p[i]];
i = p[i];
}
i
}
for &(a, b) in &segs {
let (ra, rb) = (root(&mut parent, a), root(&mut parent, b));
if ra != rb {
parent[ra.max(rb)] = ra.min(rb);
}
}
let component: Vec<usize> = (0..nn).map(|i| root(&mut parent, i)).collect();
let mut dual = vec![0.0; nn];
let mut degree = vec![0usize; nn];
let seg_len = |s: (usize, usize)| {
let (a, b) = (nodes[s.0], nodes[s.1]);
norm(core::array::from_fn(|d| b[d] - a[d]))
};
for &s in &segs {
let l = seg_len(s);
dual[s.0] += 0.5 * l;
dual[s.1] += 0.5 * l;
degree[s.0] += 1;
degree[s.1] += 1;
}
let geo = Geometry {
nodes: &nodes,
segs: &segs,
radius: &radius,
component: &component,
};
let (cap_len, flux) = correction::corrections(lines, &geo, &dual, WINDOW);
let mut slot: BTreeMap<(usize, [usize; 3]), usize> = BTreeMap::new();
let mut edges = Vec::new();
let mut p = Vec::with_capacity(segs.len());
for &(a, b) in &segs {
let mut w = BTreeMap::new();
deposit::segment_weights(lines, nodes[a], nodes[b], &mut w);
let mut row = Vec::with_capacity(w.len());
for ((d, node), v) in w {
let axis = Axis::ALL[d];
let k = *slot.entry((d, node)).or_insert_with(|| {
edges.push((axis, node));
edges.len() - 1
});
row.push((k, v / grid.edge_length(axis, node)));
}
p.push(row);
}
let mut pt = vec![Vec::new(); edges.len()];
for (s, row) in p.iter().enumerate() {
for &(k, v) in row {
pt[k].push((s, v));
}
}
let gi = |n: [usize; 3]| grid.index(n);
let seg_medium = |s: usize| {
let (mut we, mut e, mut m) = (0.0, 0.0, 0.0);
for &(k, v) in &p[s] {
let (axis, node) = edges[k];
let w = v.abs();
let i = gi(node);
we += w;
e += w * materials.eps_r(axis)[i];
let (o1, o2) = axis.others();
m += w * 0.5 * (materials.mu_r(o1)[i] + materials.mu_r(o2)[i]);
}
if we > 0.0 {
(e / we, m / we)
} else {
(1.0, 1.0)
}
};
let medium: Vec<(f64, f64)> = (0..segs.len()).map(seg_medium).collect();
let mut l = Vec::with_capacity(segs.len());
for (s, &f) in flux.iter().enumerate() {
if !(f.is_finite() && f > 0.0) {
return Err(Error::Invalid(format!(
"oblique wire #{}: radius {:e} m is too large for the thin-wire model \
(no positive inductance correction at segment {s})",
seg_wire[s], radius[s]
)));
}
l.push(MU0 * medium[s].1 * f);
}
let on_conductor = |q: [f64; 3]| -> bool {
let pec = |axis: Axis, node: [usize; 3]| {
let n = grid.nodes();
let a = axis.index();
node.iter().zip(&n).all(|(i, m)| i < m)
&& node[a] + 1 < n[a]
&& (materials.is_pec_edge(axis, node) || contact(axis, node))
};
let snap: [Option<usize>; 3] = core::array::from_fn(|d| {
let ls = lines[d];
let c = deposit::cell(ls, q[d]);
let h = ls[c + 1] - ls[c];
[c, c + 1]
.into_iter()
.find(|&i| (ls[i] - q[d]).abs() <= 1e-6 * h)
});
let base: [usize; 3] =
core::array::from_fn(|d| snap[d].unwrap_or_else(|| deposit::cell(lines[d], q[d])));
let free: Vec<usize> = (0..3).filter(|&d| snap[d].is_none()).collect();
match free[..] {
[] => Axis::ALL.iter().any(|&axis| {
let a = axis.index();
let mut back = base;
pec(axis, base)
|| (base[a] > 0 && {
back[a] -= 1;
pec(axis, back)
})
}),
[d] => pec(Axis::ALL[d], base),
[d1, d2] => {
let (a1, a2) = (Axis::ALL[d1], Axis::ALL[d2]);
let mut up1 = base;
up1[d1] += 1;
let mut up2 = base;
up2[d2] += 1;
pec(a1, base) && pec(a1, up2) && pec(a2, base) && pec(a2, up1)
}
_ => false,
}
};
let connected: Vec<bool> = (0..nn)
.map(|j| degree[j] == 1 && on_conductor(nodes[j]))
.collect();
let mut c_inv = vec![0.0; nn];
for j in 0..nn {
if connected[j] {
continue;
}
if !(cap_len[j].is_finite() && cap_len[j] > 0.0) {
return Err(Error::Invalid(format!(
"oblique wire: radius too large for the thin-wire model (no positive \
capacitance correction at node {:?})",
nodes[j]
)));
}
let (mut e, mut k) = (0.0, 0.0);
for (s, &(a, b)) in segs.iter().enumerate() {
if a == j || b == j {
e += medium[s].0;
k += 1.0;
}
}
let eps = EPS0 * e / k;
c_inv[j] = 1.0 / (eps * cap_len[j]);
}
let ns = segs.len();
let ne = edges.len();
let nfeeds = feeds.len();
Ok(Self {
nodes,
segs,
seg_wire,
l,
r,
c_inv,
connected,
edges,
p,
pt,
feeds,
current: vec![0.0; ns],
potential: vec![0.0; nn],
u_old: vec![0.0; ne],
ustar: vec![0.0; ne],
avg: vec![0.0; ns],
src: vec![0.0; nfeeds],
solver: None,
first_step: None,
dt: 0.0,
})
}
pub fn segments(&self) -> usize {
self.segs.len()
}
pub fn nodes(&self) -> &[[f64; 3]] {
&self.nodes
}
pub fn segment_nodes(&self) -> &[(usize, usize)] {
&self.segs
}
pub fn segment_wires(&self) -> &[usize] {
&self.seg_wire
}
pub fn connected_nodes(&self) -> &[bool] {
&self.connected
}
pub fn inductances(&self) -> &[f64] {
&self.l
}
pub fn capacitances(&self) -> Vec<f64> {
self.c_inv
.iter()
.map(|&c| if c > 0.0 { 1.0 / c } else { f64::INFINITY })
.collect()
}
pub fn coupled_edges(&self) -> usize {
self.edges.len()
}
pub fn currents(&self) -> &[f64] {
&self.current
}
pub fn potentials(&self) -> &[f64] {
&self.potential
}
pub fn feeds(&self) -> usize {
self.feeds.len()
}
pub fn feed_voltage(&self, k: usize) -> &[f64] {
&self.feeds[k].voltage
}
pub fn feed_current(&self, k: usize) -> &[f64] {
&self.feeds[k].current
}
pub fn feed_t0(&self) -> f64 {
(self.first_step.unwrap_or(0) as f64 + 0.5) * self.dt
}
pub fn feed_impedance(&self, k: usize, freqs: &[f64]) -> Vec<Complex64> {
let t0 = self.feed_t0();
let v = crate::post::dft(t0, self.dt, self.feed_voltage(k), freqs);
let i = crate::post::dft(t0, self.dt, self.feed_current(k), freqs);
v.iter().zip(&i).map(|(v, i)| *v / *i).collect()
}
pub fn wire_energy(&self) -> f64 {
let mut w = 0.0;
for (l, i) in self.l.iter().zip(&self.current) {
w += 0.5 * l * i * i;
}
for (c, v) in self.c_inv.iter().zip(&self.potential) {
if *c > 0.0 {
w += 0.5 * v * v / c;
}
}
w
}
fn setup(&mut self, ctx: &StepContext<'_>) -> Result<()> {
let dt = ctx.dt;
let co = ctx.coefficients;
let cb: Vec<f64> = self
.edges
.iter()
.map(|&(axis, node)| co.cb(axis)[ctx.grid.index(node)] as f64)
.collect();
let mut lower: BTreeMap<(usize, usize), f64> = BTreeMap::new();
let mut add = |i: usize, j: usize, v: f64| {
let k = if j <= i { (i, j) } else { (j, i) };
*lower.entry(k).or_insert(0.0) += v;
};
for s in 0..self.segs.len() {
add(s, s, 2.0 * self.l[s] / dt + self.r[s]);
}
for (k, col) in self.pt.iter().enumerate() {
for (x, &(s1, p1)) in col.iter().enumerate() {
for &(s2, p2) in &col[x..] {
let v = 0.5 * cb[k] * p1 * p2;
add(s1, s2, v);
}
}
}
let mut incident: Vec<Vec<(usize, f64)>> = vec![Vec::new(); self.nodes.len()];
for (s, &(a, b)) in self.segs.iter().enumerate() {
incident[a].push((s, -1.0));
incident[b].push((s, 1.0));
}
for (j, inc) in incident.iter().enumerate() {
let c = self.c_inv[j];
if c == 0.0 {
continue;
}
for (x, &(s1, g1)) in inc.iter().enumerate() {
for &(s2, g2) in &inc[x..] {
add(s1, s2, 0.5 * dt * c * g1 * g2);
}
}
}
let chol = Envelope::factor(self.segs.len(), &lower)?;
self.dt = dt;
self.solver = Some(Solver { dt, cb, chol });
Ok(())
}
}
impl Operator for ObliqueWires {
fn hooks(&self) -> Hooks {
Hooks::PRE_E | Hooks::POST_E
}
fn energy(&self) -> f64 {
self.wire_energy()
}
fn pre_e(&mut self, fields: &mut Fields<'_>, _ctx: &StepContext<'_>) {
for (u, &(axis, node)) in self.u_old.iter_mut().zip(&self.edges) {
*u = fields.u.get(axis, node) as f64;
}
}
fn post_e(&mut self, fields: &mut Fields<'_>, ctx: &StepContext<'_>) {
if self.solver.is_none() {
self.setup(ctx)
.expect("oblique wire system is positive definite");
self.first_step = Some(ctx.step);
}
let Some(sv) = &self.solver else { return };
let dt = sv.dt;
for (u, &(axis, node)) in self.ustar.iter_mut().zip(&self.edges) {
*u = fields.u.get(axis, node) as f64;
}
let rhs = &mut self.avg;
for (s, row) in self.p.iter().enumerate() {
let mut v = 2.0 * self.l[s] / dt * self.current[s];
for &(k, pv) in row {
v += 0.5 * pv * (self.ustar[k] + self.u_old[k]);
}
let (a, b) = self.segs[s];
v -= self.potential[b] - self.potential[a];
rhs[s] = v;
}
let t = ctx.h_time();
for (f, v) in self.feeds.iter().zip(&mut self.src) {
*v = f.source.as_ref().map_or(0.0, |(w, a)| a * w.value(t, dt));
rhs[f.segment] += *v;
}
sv.chol.solve(rhs);
let avg = &self.avg;
for (k, col) in self.pt.iter().enumerate() {
let mut j = 0.0;
for &(s, pv) in col {
j += pv * avg[s];
}
if j != 0.0 && sv.cb[k] != 0.0 {
let (axis, node) = self.edges[k];
fields
.u
.set(axis, node, (self.ustar[k] - sv.cb[k] * j) as f32);
}
}
for (s, &(a, b)) in self.segs.iter().enumerate() {
let jb = avg[s];
self.current[s] = 2.0 * jb - self.current[s];
self.potential[a] -= dt * self.c_inv[a] * jb;
self.potential[b] += dt * self.c_inv[b] * jb;
}
for (f, &v) in self.feeds.iter_mut().zip(&self.src) {
let jb = avg[f.segment];
f.voltage.push(v - f.resistance * jb);
f.current.push(jb);
}
}
}