use super::Engine;
use super::fields::Fields;
use super::kernel::flush;
use super::operator::{Hooks, Operator, OperatorHandle, StepContext};
use crate::axis::Axis;
use crate::constants::{EPS0, MU0};
use crate::post::Complex64;
use crate::{Error, Result};
pub const SURFACE_LAYER: f64 = 0.15;
pub const GROWTH: f64 = 1.15;
pub fn skin_depth(sigma: f64, f: f64) -> f64 {
(2.0 / (2.0 * core::f64::consts::PI * f * MU0 * sigma)).sqrt()
}
pub fn layer_profile(sigma: f64, thickness: f64, f_max: f64) -> Vec<f64> {
let s = SURFACE_LAYER * skin_depth(sigma, f_max);
if !(s.is_finite() && s > 0.0) || thickness <= 2.0 * s {
let n = if s.is_finite() && s > 0.0 {
((thickness / s).ceil() as usize).max(1)
} else {
1
};
return vec![thickness / n as f64; n];
}
let half_t = 0.5 * thickness;
let mut half = Vec::new();
let (mut acc, mut dz) = (0.0, s);
while acc + dz < half_t {
half.push(dz);
acc += dz;
dz *= GROWTH;
}
let rem = half_t - acc;
match half.last_mut() {
Some(last) if rem < 0.5 * *last => *last += rem,
_ => half.push(rem),
}
let mut out = half.clone();
out.extend(half.iter().rev());
out
}
fn coth_csch(x: Complex64) -> (Complex64, Complex64) {
let e1 = (-x).exp();
let e2 = e1 * e1;
let den = Complex64::ONE - e2;
((Complex64::ONE + e2) / den, e1 * 2.0 / den)
}
fn csqrt(z: Complex64) -> Complex64 {
let (r, a) = z.to_polar();
Complex64::from_polar(r.sqrt(), 0.5 * a)
}
pub fn slab_impedance(sigma: f64, t: f64, f: f64) -> (Complex64, Complex64) {
let w = 2.0 * core::f64::consts::PI * f;
let y = Complex64::new(sigma, w * EPS0);
let jwm = Complex64::new(0.0, w * MU0);
let gamma = csqrt(jwm * y);
let zc = csqrt(jwm / y);
let (coth, csch) = coth_csch(gamma * t);
(zc * coth, zc * csch)
}
pub fn slab_abcd(sigma: f64, t: f64, f: f64) -> [Complex64; 4] {
let (z11, z12) = slab_impedance(sigma, t, f);
z_to_abcd(z11, z12)
}
fn z_to_abcd(z11: Complex64, z12: Complex64) -> [Complex64; 4] {
let a = z11 / z12;
[a, (z11 * z11 - z12 * z12) / z12, z12.inv(), a]
}
pub fn model_abcd(sigma: f64, t: f64, f_max: f64, f: f64) -> [Complex64; 4] {
let w = 2.0 * core::f64::consts::PI * f;
let dz = layer_profile(sigma, t, f_max);
let mul = |m: [Complex64; 4], n: [Complex64; 4]| {
[
m[0] * n[0] + m[1] * n[2],
m[0] * n[1] + m[1] * n[3],
m[2] * n[0] + m[3] * n[2],
m[2] * n[1] + m[3] * n[3],
]
};
let shunt = |v: f64| {
let y = Complex64::new(sigma * v, w * EPS0 * v);
[Complex64::ONE, Complex64::ZERO, y, Complex64::ONE]
};
let series = |l: f64| {
let z = Complex64::new(0.0, w * MU0 * l);
[Complex64::ONE, z, Complex64::ZERO, Complex64::ONE]
};
let mut m = shunt(0.5 * dz[0]);
for (k, &d) in dz.iter().enumerate() {
m = mul(m, series(d));
let v = 0.5 * (d + dz.get(k + 1).copied().unwrap_or(0.0));
m = mul(m, shunt(v));
}
m
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SheetEdge {
pub axis: Axis,
pub node: [usize; 3],
pub normal: Axis,
pub conductivity: f64,
pub thickness: f64,
pub upper_share: Option<(f64, f64)>,
}
#[derive(Debug, Clone, Copy)]
struct Coupling {
axis: Axis,
node: [usize; 3],
w_up: f64,
w_dn: f64,
corr: f64,
ecorr: f64,
}
#[derive(Debug, Clone)]
struct EdgeState {
axis: Axis,
node: [usize; 3],
c_edge: f64,
n0: usize,
nn: usize,
l0: usize,
c0: usize,
nc: usize,
}
#[derive(Debug, Default)]
pub struct ConductingSheet {
f_max: Option<f64>,
entries: Vec<SheetEdge>,
installed: bool,
skipped: usize,
dt: f64,
edges: Vec<EdgeState>,
couplings: Vec<Coupling>,
two_c: Vec<f64>,
cp: Vec<f64>,
inv_den: Vec<f64>,
u: Vec<f64>,
a: Vec<f64>,
h: Vec<f64>,
scratch: Vec<f64>,
w_corr: f64,
}
impl ConductingSheet {
pub fn new(f_max: Option<f64>) -> Self {
Self {
f_max,
..Self::default()
}
}
pub fn push(&mut self, e: SheetEdge) {
self.entries.push(e);
}
pub fn add_rect(
&mut self,
normal: Axis,
plane: usize,
start: [usize; 3],
stop: [usize; 3],
sigma: f64,
t: f64,
) {
let (p, q) = normal.others();
for (d, o) in [(p, q), (q, p)] {
let (di, oi) = (d.index(), o.index());
for a in start[di]..stop[di] {
for b in start[oi]..=stop[oi] {
let mut node = [0; 3];
node[normal.index()] = plane;
node[di] = a;
node[oi] = b;
self.push(SheetEdge {
axis: d,
node,
normal,
conductivity: sigma,
thickness: t,
upper_share: None,
});
}
}
}
}
pub fn entries(&self) -> &[SheetEdge] {
&self.entries
}
pub fn edges(&self) -> usize {
self.edges.len()
}
pub fn skipped(&self) -> usize {
self.skipped
}
pub fn ladder_nodes(&self) -> usize {
self.u.len()
}
pub fn lower_voltage(&self, axis: Axis, node: [usize; 3]) -> Option<f64> {
self.edges
.iter()
.find(|e| e.axis == axis && e.node == node)
.map(|e| self.u[e.n0 + e.nn - 1])
}
fn validate(e: &SheetEdge, n: [usize; 3]) -> Result<()> {
let bad = |m: String| Err(Error::Invalid(format!("conducting sheet edge {e:?}: {m}")));
if e.axis == e.normal {
return bad("the edge is not tangential to the sheet".into());
}
if !(e.conductivity.is_finite() && e.conductivity > 0.0) {
return bad("conductivity must be finite and > 0".into());
}
if !(e.thickness.is_finite() && e.thickness > 0.0) {
return bad("thickness must be finite and > 0".into());
}
if let Some((c, g)) = e.upper_share
&& !((0.0..=1.0).contains(&c) && (0.0..=1.0).contains(&g))
{
return bad("shares must be in [0, 1]".into());
}
let a = e.axis.index();
if (0..3).any(|t| e.node[t] >= n[t]) || e.node[a] + 1 >= n[a] {
return bad(format!("not an edge of the grid ({n:?} nodes)"));
}
Ok(())
}
fn install(&mut self, engine: &Engine) -> Result<()> {
if self.installed {
return Err(Error::Invalid("conducting sheet already installed".into()));
}
let grid = engine.grid();
let n = grid.nodes();
for e in &self.entries {
Self::validate(e, n)?;
}
let dt = engine.dt();
let f_max = match self.f_max {
Some(f) if f.is_finite() && f > 0.0 => f,
Some(f) => {
return Err(Error::Invalid(format!(
"conducting sheet: f_max {f} must be finite and > 0"
)));
}
None => 0.1 / dt,
};
let mut order: Vec<usize> = (0..self.entries.len()).collect();
let key = |e: &SheetEdge| (e.node, e.axis.index());
order.sort_by_key(|&i| key(&self.entries[i]));
let co = engine.coefficients();
let mut last = None;
for &i in &order {
let e = self.entries[i];
if last == Some(key(&e)) {
self.skipped += 1;
continue;
}
last = Some(key(&e));
let idx = grid.index(e.node);
let (ca, cb) = (co.ca(e.axis)[idx], co.cb(e.axis)[idx]);
if cb == 0.0 {
self.skipped += 1;
continue;
}
let c_edge = dt * (1.0 + ca as f64) / (2.0 * cb as f64);
let g_edge = (1.0 - ca as f64) / cb as f64;
let (nrm, d) = (e.normal, e.axis);
let t_ax = Axis::ALL[3 - nrm.index() - d.index()];
let k = e.node[nrm.index()];
let up = if k + 1 < n[nrm.index()] {
grid.delta(nrm, k)
} else {
0.0
};
let dn = if k > 0 { grid.delta(nrm, k - 1) } else { 0.0 };
let f_up = up / (up + dn);
let (sc, sg) = e.upper_share.unwrap_or((f_up, f_up));
let geo =
grid.dual_delta(t_ax, e.node[t_ax.index()]) / grid.delta(d, e.node[d.index()]);
let dz = layer_profile(e.conductivity, e.thickness, f_max);
let nn = dz.len() + 1;
let (n0, l0) = (self.u.len(), self.a.len());
let mut g_node = vec![0.0; nn];
let mut c_node = vec![0.0; nn];
for (j, &z) in dz.iter().enumerate() {
for kk in [j, j + 1] {
g_node[kk] += 0.5 * e.conductivity * z * geo;
c_node[kk] += 0.5 * EPS0 * z * geo;
}
self.a.push(dt * geo / (2.0 * MU0 * z));
self.h.push(0.0);
}
c_node[0] += sc * c_edge;
g_node[0] += sg * g_edge;
c_node[nn - 1] += (1.0 - sc) * c_edge;
g_node[nn - 1] += (1.0 - sg) * g_edge;
let a = &self.a[l0..];
let mut prev_cp = 0.0;
for kk in 0..nn {
let a_up = if kk + 1 < nn { a[kk] } else { 0.0 };
let a_dn = if kk > 0 { a[kk - 1] } else { 0.0 };
let two_c = 2.0 * c_node[kk] / dt;
let diag = two_c + g_node[kk] + a_up + a_dn;
let den = diag + a_dn * prev_cp;
let cp = -a_up / den;
self.two_c.push(two_c);
self.cp.push(cp);
self.inv_den.push(1.0 / den);
self.u.push(0.0);
prev_cp = cp;
}
let (b, c) = d.others();
let c0 = self.couplings.len();
for (ax, off, s) in [
(c, None, 1.0),
(c, Some(b), -1.0),
(b, None, -1.0),
(b, Some(c), 1.0),
] {
let mut m = e.node;
if let Some(o) = off {
if m[o.index()] == 0 {
continue;
}
m[o.index()] -= 1;
}
let (p, q) = ax.others();
if m[p.index()] + 1 >= n[p.index()] || m[q.index()] + 1 >= n[q.index()] {
continue;
}
let (w_up, w_dn) = if ax == nrm {
(f_up, 1.0 - f_up)
} else if off == Some(nrm) {
(0.0, 1.0)
} else {
(1.0, 0.0)
};
let mi = grid.index(m);
let (da, db) = (co.da(ax)[mi] as f64, co.db(ax)[mi] as f64);
let corr = -db * s * w_dn;
let ecorr = if db == 0.0 {
0.0
} else {
0.5 * dt * (1.0 + da) / (2.0 * db) * corr
};
self.couplings.push(Coupling {
axis: ax,
node: m,
w_up: s * w_up,
w_dn: s * w_dn,
corr,
ecorr,
});
}
self.edges.push(EdgeState {
axis: d,
node: e.node,
c_edge,
n0,
nn,
l0,
c0,
nc: self.couplings.len() - c0,
});
}
drop(co);
let max_nn = self.edges.iter().map(|e| e.nn).max().unwrap_or(0);
self.scratch = vec![0.0; max_nn];
self.dt = dt;
self.installed = true;
Ok(())
}
pub fn add_to(mut self, engine: &mut Engine) -> Result<OperatorHandle<ConductingSheet>> {
self.install(engine)?;
Ok(engine.add_operator(self))
}
}
impl Operator for ConductingSheet {
fn hooks(&self) -> Hooks {
if self.edges.is_empty() {
Hooks::NONE
} else {
Hooks::POST_H | Hooks::POST_E
}
}
fn post_h(&mut self, f: &mut Fields<'_>, _ctx: &StepContext<'_>) {
for e in &self.edges {
let up = f.u.get(e.axis, e.node) as f64;
self.u[e.n0] = up;
let diff = self.u[e.n0 + e.nn - 1] - up;
for c in &self.couplings[e.c0..e.c0 + e.nc] {
if c.corr != 0.0 {
let v = f.i.get_mut(c.axis, c.node);
*v = flush(*v + (c.corr * diff) as f32);
}
}
}
}
fn post_e(&mut self, f: &mut Fields<'_>, _ctx: &StepContext<'_>) {
let x = &mut self.scratch;
let mut w_corr = 0.0;
for e in &self.edges {
let (mut s_up, mut s_dn, mut s_e) = (0.0, 0.0, 0.0);
for c in &self.couplings[e.c0..e.c0 + e.nc] {
let i = f.i.get(c.axis, c.node) as f64;
s_up += c.w_up * i;
s_dn += c.w_dn * i;
s_e += c.ecorr * i;
}
let (n0, nn, l0) = (e.n0, e.nn, e.l0);
let u = &mut self.u[n0..n0 + nn];
let (two_c, cp, inv_den) = (
&self.two_c[n0..n0 + nn],
&self.cp[n0..n0 + nn],
&self.inv_den[n0..n0 + nn],
);
let a = &self.a[l0..l0 + nn - 1];
let h = &mut self.h[l0..l0 + nn - 1];
for k in 0..nn {
let mut r = two_c[k] * u[k];
if k + 1 < nn {
r -= h[k];
}
if k > 0 {
r += h[k - 1] + a[k - 1] * x[k - 1];
}
if k == 0 {
r += s_up;
}
if k + 1 == nn {
r += s_dn;
}
x[k] = r * inv_den[k];
}
for k in (0..nn - 1).rev() {
x[k] -= cp[k] * x[k + 1];
}
for j in 0..nn - 1 {
h[j] += 2.0 * a[j] * (x[j] - x[j + 1]);
}
for k in 0..nn {
u[k] = 2.0 * x[k] - u[k];
}
let up = flush(u[0] as f32);
f.u.set(e.axis, e.node, up);
w_corr += s_e * (u[nn - 1] - up as f64);
}
self.w_corr = w_corr;
}
fn energy(&self) -> f64 {
let mut w = 0.0;
for e in &self.edges {
let (n0, nn, l0) = (e.n0, e.nn, e.l0);
for k in 0..nn {
w += 0.25 * self.two_c[k + n0] * self.dt * self.u[n0 + k] * self.u[n0 + k];
}
w -= 0.5 * e.c_edge * self.u[n0] * self.u[n0];
for j in 0..nn - 1 {
let l = self.dt / (2.0 * self.a[l0 + j]);
w += 0.5 * l * self.h[l0 + j] * self.h[l0 + j];
}
}
w + self.w_corr
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_sums_and_grades() {
for (s, t, f) in [(5.8e7, 35e-6, 10e9), (5.8e7, 35e-9, 1e9), (1e5, 1e-3, 1e10)] {
let p = layer_profile(s, t, f);
let sum: f64 = p.iter().sum();
assert!((sum - t).abs() < 1e-12 * t, "{s} {t}: {sum}");
assert!(p.iter().all(|&d| d > 0.0));
}
assert_eq!(layer_profile(5.8e7, 35e-9, 1e9).len(), 1);
assert_eq!(layer_profile(5.8e7, 35e-6, 10e9).len(), 48);
}
#[test]
fn thin_and_thick_limits() {
let (z11, z12) = slab_impedance(1e4, 1e-6, 1e9);
let r = 1.0 / (1e4 * 1e-6);
assert!((z11.re / r - 1.0).abs() < 1e-3 && (z12.re / r - 1.0).abs() < 1e-3);
let (s, f) = (5.8e7, 10e9);
let (z11, z12) = slab_impedance(s, 1e-3, f);
let rs = 1.0 / (s * skin_depth(s, f));
assert!((z11 - Complex64::new(rs, rs)).norm() < 1e-6 * rs);
assert!(z12.norm() < 1e-100);
}
#[test]
fn ladder_matches_slab() {
for (s, t, fmax) in [
(5.8e7, 35e-6, 10e9),
(5.8e7, 1e-6, 10e9),
(5.8e7, 5e-6, 10e9),
(1e5, 1e-3, 10e9),
(1e3, 1e-4, 1e10),
] {
let mut worst: f64 = 0.0;
for i in 1..=40 {
let f = fmax * i as f64 / 40.0;
let m = model_abcd(s, t, fmax, f);
let x = slab_abcd(s, t, f);
let za = m[0] / m[2];
let ze = x[0] / x[2];
worst = worst.max((za / ze - 1.0).norm());
}
assert!(worst < 0.012, "{s} {t}: Z11 error {worst}");
}
}
}