use std::ops::Deref;
use csx::primitives::BoxShape;
use csx::properties::{Excitation, LumpedElement, ProbeBox, ProbeType};
use csx::{ContinuousStructure, Primitive, Property, PropertyId, Scalar};
use crate::axis::Axis;
use crate::constants::C0;
use crate::post::{Complex64, PortResult};
use crate::sim::Results;
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MslPortProperties {
pub metal: (String, PropertyId),
pub element: Option<(String, PropertyId)>,
pub excitation: Option<(String, PropertyId)>,
pub voltages: [(String, PropertyId); 3],
pub currents: [(String, PropertyId); 2],
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Layout {
v_pos: [f64; 3],
spacing: f64,
s_mid: f64,
s_ref: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MslPort {
number: u32,
start: [f64; 3],
stop: [f64; 3],
dir: Axis,
height: Axis,
metal: Option<PropertyId>,
feed_r: Option<f64>,
amplitude: Option<f64>,
delay: f64,
priority: i32,
shift: f64,
props: Option<MslPortProperties>,
layout: Option<Layout>,
}
impl MslPort {
pub fn new(number: u32, start: [f64; 3], stop: [f64; 3], dir: Axis, height: Axis) -> Self {
Self {
number,
start,
stop,
dir,
height,
metal: None,
feed_r: None,
amplitude: None,
delay: 0.0,
priority: 0,
shift: 0.0,
props: None,
layout: None,
}
}
pub fn metal(mut self, id: PropertyId) -> Self {
self.metal = Some(id);
self
}
pub fn feed_resistance(mut self, r: f64) -> Self {
self.feed_r = (r != 0.0).then_some(r);
self
}
pub fn excite(mut self, amplitude: f64) -> Self {
self.amplitude = Some(amplitude);
self
}
pub fn delay(mut self, delay: f64) -> Self {
self.delay = delay;
self
}
pub fn priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn reference_shift(mut self, shift: f64) -> Self {
self.shift = shift;
self
}
pub fn number(&self) -> u32 {
self.number
}
pub fn is_excited(&self) -> bool {
self.amplitude.is_some()
}
pub fn properties(&self) -> Option<&MslPortProperties> {
self.props.as_ref()
}
pub fn probe_positions(&self) -> Option<[f64; 3]> {
self.layout.map(|l| l.v_pos)
}
pub fn mesh_lines(&self) -> [Vec<f64>; 3] {
let mut out: [Vec<f64>; 3] = Default::default();
for d in [self.dir.index(), self.height.index()] {
out[d] = vec![self.start[d], self.stop[d]];
out[d].sort_by(f64::total_cmp);
out[d].dedup();
}
out
}
fn width_axis(&self) -> Result<Axis> {
if self.dir == self.height {
return Err(Error::Invalid(format!(
"MSL port {}: propagation and height axes are both {}",
self.number, self.dir
)));
}
Ok(Axis::ALL
.into_iter()
.find(|&a| a != self.dir && a != self.height)
.expect("three axes"))
}
pub fn add_to(mut self, model: &mut ContinuousStructure) -> Result<Self> {
let n = self.number;
if self.props.is_some() {
return Err(Error::Invalid(format!("MSL port {n} was already added")));
}
if model.grid.coord_system != csx::CoordSystem::Cartesian {
return Err(Error::Unsupported(format!(
"MSL port {n}: MSL ports need a Cartesian grid"
)));
}
let w_axis = self.width_axis()?;
if self.start.iter().chain(&self.stop).any(|v| !v.is_finite())
|| !self.delay.is_finite()
|| !self.shift.is_finite()
|| self.amplitude.is_some_and(|a| !a.is_finite())
{
return Err(Error::Invalid(format!(
"MSL port {n}: values must be finite"
)));
}
if let Some(r) = self.feed_r
&& !(r.is_finite() && r > 0.0)
{
return Err(Error::Invalid(format!(
"MSL port {n}: feed resistance {r} must be finite and > 0"
)));
}
let (p, h, w) = (self.dir.index(), self.height.index(), w_axis.index());
let du = model.grid.delta_unit;
let (feed, end) = (self.start[p], self.stop[p]);
if end == feed {
return Err(Error::Invalid(format!(
"MSL port {n} has zero length along {}",
self.dir
)));
}
let (h_gnd, h_strip) = (self.start[h], self.stop[h]);
if h_gnd == h_strip {
return Err(Error::Invalid(format!(
"MSL port {n} has zero height along {}",
self.height
)));
}
let (w_lo, w_hi) = (
self.start[w].min(self.stop[w]),
self.start[w].max(self.stop[w]),
);
for (axis, x, what) in [
(self.dir, feed, "feed plane"),
(self.height, h_gnd, "ground"),
(self.height, h_strip, "strip height"),
] {
if line_index(model.grid.lines(axis.index()), x).is_none() {
return Err(Error::Invalid(format!(
"MSL port {n}: the {what} {axis} = {x} is not a mesh line \
(mesh the model first; see MslPort::mesh_lines)"
)));
}
}
let layout =
choose_probes(model.grid.lines(p), feed, end, self.shift, du).ok_or_else(|| {
Error::Invalid(format!(
"MSL port {n}: needs three equally spaced mesh lines along {} \
past the feed at {feed} and not beyond {end}",
self.dir
))
})?;
let in_strip: Vec<f64> = model
.grid
.lines(w)
.iter()
.copied()
.filter(|&x| {
(w_lo..=w_hi).contains(&x)
|| line_index(&[x], w_lo).is_some()
|| line_index(&[x], w_hi).is_some()
})
.collect();
let (Some(&s_lo), Some(&s_hi)) = (in_strip.first(), in_strip.last()) else {
return Err(Error::Invalid(format!(
"MSL port {n}: no {w_axis} mesh line within the strip {w_lo} ..= {w_hi}"
)));
};
let w_mid = 0.5 * (w_lo + w_hi);
let w_c = in_strip
.iter()
.copied()
.min_by(|x, y| (x - w_mid).abs().total_cmp(&(y - w_mid).abs()))
.expect("non-empty");
let names = [
format!("port_metal_{n}"),
format!("port_resist_{n}"),
format!("port_excite_{n}"),
format!("port_ut{n}A"),
format!("port_ut{n}B"),
format!("port_ut{n}C"),
format!("port_it{n}A"),
format!("port_it{n}B"),
];
let used = |i: usize| match i {
0 => self.metal.is_none(),
1 => self.feed_r.is_some(),
2 => self.amplitude.is_some(),
_ => true,
};
if let Some(dup) = names
.iter()
.enumerate()
.find(|&(i, s)| used(i) && model.property_by_name(s).is_some())
{
return Err(Error::Invalid(format!(
"the model already has a property named {:?}",
dup.1
)));
}
let metal = match self.metal {
Some(id) => {
let prop = model.property(id).ok_or_else(|| {
Error::Invalid(format!("MSL port {n}: no metal property {id}"))
})?;
(prop.name.clone(), id)
}
None => {
let id = model.add_property(Property::metal(names[0].clone()));
(names[0].clone(), id)
}
};
let csx_axis = |a: usize| csx::Axis::from_index(a).expect("axis index in 0..3");
let prim = |s: [f64; 3], t: [f64; 3]| {
Primitive::new(BoxShape::new(s, t)).with_priority(self.priority)
};
let point = |xp: f64, xw: f64, xh: f64| {
let mut v = [0.0; 3];
v[p] = xp;
v[w] = xw;
v[h] = xh;
v
};
model.add_primitive(
metal.1,
prim(point(feed, w_lo, h_strip), point(end, w_hi, h_strip)),
)?;
let (f0, f1) = (point(feed, w_lo, h_strip), point(feed, w_hi, h_gnd));
let element = match self.feed_r {
Some(r) => {
let el = LumpedElement::resistor(csx_axis(h), r);
let id = model.add_property(Property::lumped_element(names[1].clone(), el));
model.add_primitive(id, prim(f0, f1))?;
Some((names[1].clone(), id))
}
None => None,
};
let excitation = match self.amplitude {
Some(amp) => {
let height = (h_strip - h_gnd).abs() * du;
let mut v = [0.0; 3];
v[h] = (h_gnd - h_strip).signum() * amp / height;
let mut ex = Excitation::new(v).with_number(n);
ex.delay = Scalar::new(self.delay);
let id = model.add_property(Property::excitation(names[2].clone(), ex));
model.add_primitive(id, prim(f0, f1))?;
Some((names[2].clone(), id))
}
None => None,
};
let voltages = [0, 1, 2].map(|k| {
let mut pb = ProbeBox::new(ProbeType::Voltage);
pb.number = n;
let id = model.add_property(Property::probe(names[3 + k].clone(), pb));
(names[3 + k].clone(), id)
});
for (k, (_, id)) in voltages.iter().enumerate() {
let x = layout.v_pos[k];
model.add_primitive(*id, prim(point(x, w_c, h_strip), point(x, w_c, h_gnd)))?;
}
let currents = [0, 1].map(|k| {
let mut pb = ProbeBox::new(ProbeType::Current).with_norm_dir(csx_axis(p));
pb.number = n;
let id = model.add_property(Property::probe(names[6 + k].clone(), pb));
(names[6 + k].clone(), id)
});
for (k, (_, id)) in currents.iter().enumerate() {
let (a, b) = (layout.v_pos[k], layout.v_pos[k + 1]);
model.add_primitive(*id, prim(point(a, s_lo, h_strip), point(b, s_hi, h_strip)))?;
}
self.props = Some(MslPortProperties {
metal,
element,
excitation,
voltages,
currents,
});
self.layout = Some(layout);
Ok(self)
}
pub fn analyze(&self, results: &Results, freqs: &[f64]) -> Result<MslPortResult> {
let line = self.line(results, freqs)?;
let port = PortResult::new_per_frequency(
freqs.to_vec(),
line.v.clone(),
line.i.clone(),
line.z_line.clone(),
)
.map_err(|e| {
Error::Invalid(format!(
"MSL port {}: unusable line impedance ({e}); is every frequency inside \
the excited band?",
self.number
))
})?;
Ok(line.finish(port))
}
pub fn analyze_with(
&self,
results: &Results,
freqs: &[f64],
z_ref: Complex64,
) -> Result<MslPortResult> {
let line = self.line(results, freqs)?;
let port = PortResult::new(freqs.to_vec(), line.v.clone(), line.i.clone(), z_ref)?;
Ok(line.finish(port))
}
fn line(&self, results: &Results, freqs: &[f64]) -> Result<Line> {
let (Some(p), Some(l)) = (&self.props, &self.layout) else {
return Err(Error::Invalid(format!(
"MSL port {} was not added",
self.number
)));
};
let get = |name: &str| {
results
.probe(name)
.ok_or_else(|| Error::Invalid(format!("no probe {name:?} in the results")))?
.dft(freqs)
};
let [v1, v2, v3] = [0, 1, 2].map(|k| get(&p.voltages[k].0));
let [ia, ib] = [0, 1].map(|k| get(&p.currents[k].0));
let (v1, v2, v3, ia, ib) = (v1?, v2?, v3?, ia?, ib?);
let n = freqs.len();
let mut out = Line {
gamma: Vec::with_capacity(n),
z_line: Vec::with_capacity(n),
v: Vec::with_capacity(n),
i: Vec::with_capacity(n),
};
let delta = l.s_ref - l.s_mid;
for k in 0..n {
let (g, z, v, i) = extract([v1[k], v2[k], v3[k]], [ia[k], ib[k]], l.spacing, delta);
out.gamma.push(g);
out.z_line.push(z);
out.v.push(v);
out.i.push(i);
}
Ok(out)
}
}
struct Line {
gamma: Vec<Complex64>,
z_line: Vec<Complex64>,
v: Vec<Complex64>,
i: Vec<Complex64>,
}
impl Line {
fn finish(self, port: PortResult) -> MslPortResult {
MslPortResult {
port,
gamma: self.gamma,
z_line: self.z_line,
}
}
}
fn extract(
v: [Complex64; 3],
i: [Complex64; 2],
d: f64,
delta: f64,
) -> (Complex64, Complex64, Complex64, Complex64) {
let sh2 = (v[0] + v[2] - v[1] * 2.0) / (v[1] * 4.0);
let mut sh = sh2.sqrt();
let mut x = (sh + (sh * sh + 1.0).sqrt()).ln();
if x.im < 0.0 {
x = -x;
sh = -sh;
}
let gamma = x * (2.0 / d);
let ch = (x.exp() + (-x).exp()) * 0.5;
let isum = i[0] + i[1];
let z = -(v[2] - v[0]) / (sh * isum * 2.0);
let i_mid = isum / (ch * 2.0);
let a = (v[1] + z * i_mid) * 0.5 * (-gamma * delta).exp();
let b = (v[1] - z * i_mid) * 0.5 * (gamma * delta).exp();
(gamma, z, a + b, (a - b) / z)
}
fn line_index(lines: &[f64], x: f64) -> Option<usize> {
let (k, &l) = lines
.iter()
.enumerate()
.min_by(|a, b| (a.1 - x).abs().total_cmp(&(b.1 - x).abs()))?;
let cell = [k.checked_sub(1), Some(k + 1)]
.into_iter()
.flatten()
.filter_map(|j| lines.get(j))
.map(|&o| (o - l).abs())
.fold(f64::INFINITY, f64::min);
let tol = if cell.is_finite() {
1e-6 * cell
} else {
1e-9 * (1.0 + x.abs())
};
((l - x).abs() <= tol).then_some(k)
}
fn choose_probes(lines: &[f64], feed: f64, end: f64, shift: f64, du: f64) -> Option<Layout> {
let sign = (end - feed).signum();
let s = |x: f64| (x - feed) * sign;
let len = s(end);
let tol = 1e-6 * len;
let mut cand: Vec<f64> = lines
.iter()
.copied()
.filter(|&x| s(x) > tol && s(x) <= len + tol)
.collect();
if sign < 0.0 {
cand.reverse();
}
let mid = 0.5 * len;
cand.windows(3)
.filter(|t| {
let (d1, d2) = (s(t[1]) - s(t[0]), s(t[2]) - s(t[1]));
(d1 - d2).abs() <= 1e-6 * d1.max(d2)
})
.min_by(|a, b| (s(a[1]) - mid).abs().total_cmp(&(s(b[1]) - mid).abs()))
.map(|t| Layout {
v_pos: [t[0], t[1], t[2]],
spacing: 0.5 * (s(t[2]) - s(t[0])) * du,
s_mid: s(t[1]) * du,
s_ref: (len + shift) * du,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct MslPortResult {
port: PortResult,
gamma: Vec<Complex64>,
z_line: Vec<Complex64>,
}
impl MslPortResult {
pub fn port(&self) -> &PortResult {
&self.port
}
pub fn into_port(self) -> PortResult {
self.port
}
pub fn gamma(&self) -> &[Complex64] {
&self.gamma
}
pub fn beta(&self) -> Vec<f64> {
self.gamma.iter().map(|g| g.im).collect()
}
pub fn alpha(&self) -> Vec<f64> {
self.gamma.iter().map(|g| g.re).collect()
}
pub fn eps_eff(&self) -> Vec<f64> {
self.port
.freqs()
.iter()
.zip(&self.gamma)
.map(|(&f, g)| (C0 * g.im / (std::f64::consts::TAU * f)).powi(2))
.collect()
}
pub fn z_line(&self) -> &[Complex64] {
&self.z_line
}
}
impl Deref for MslPortResult {
type Target = PortResult;
fn deref(&self) -> &PortResult {
&self.port
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(re: f64, im: f64) -> Complex64 {
Complex64::new(re, im)
}
#[test]
fn extraction_is_exact_for_exponential_modes() {
let gamma = c(0.3, 50.0);
let z = c(48.0, -2.0);
let (a, b) = (c(1.0, 0.2), c(-0.3, 0.1));
let (d, s2) = (1e-3, 7e-3);
let v = |s: f64| a * (-gamma * s).exp() + b * (gamma * s).exp();
let i = |s: f64| (a * (-gamma * s).exp() - b * (gamma * s).exp()) / z;
let (gx, zx, vx, ix) = extract(
[v(s2 - d), v(s2), v(s2 + d)],
[i(s2 - 0.5 * d), i(s2 + 0.5 * d)],
d,
3e-3,
);
assert!((gx - gamma).norm() < 1e-9 * gamma.norm(), "{gx}");
assert!((zx - z).norm() < 1e-9 * z.norm(), "{zx}");
assert!((vx - v(s2 + 3e-3)).norm() < 1e-9);
assert!((ix - i(s2 + 3e-3)).norm() < 1e-11);
}
#[test]
fn probe_choice() {
let lines: Vec<f64> = (0..=10).map(f64::from).collect();
let l = choose_probes(&lines, 0.0, 10.0, 0.0, 1e-3).unwrap();
assert_eq!(l.v_pos, [4.0, 5.0, 6.0]);
assert!((l.spacing - 1e-3).abs() < 1e-15);
assert!((l.s_mid - 5e-3).abs() < 1e-15 && (l.s_ref - 10e-3).abs() < 1e-15);
let l = choose_probes(&lines, 10.0, 2.0, 1.0, 1e-3).unwrap();
assert_eq!(l.v_pos, [7.0, 6.0, 5.0]);
assert!((l.s_mid - 4e-3).abs() < 1e-15 && (l.s_ref - 9e-3).abs() < 1e-15);
let lines = [0.0, 1.0, 3.0, 4.0, 5.0, 7.0, 10.0];
let l = choose_probes(&lines, 0.0, 10.0, 0.0, 1.0).unwrap();
assert_eq!(l.v_pos, [3.0, 4.0, 5.0]);
let lines = [0.0, 1.0, 3.0, 6.0, 10.0];
assert!(choose_probes(&lines, 0.0, 10.0, 0.0, 1.0).is_none());
assert!(choose_probes(&[0.0, 1.0, 2.0], 0.0, 2.0, 0.0, 1.0).is_none());
}
#[test]
fn mesh_snapping() {
let lines = [0.0, 0.5, 1.0];
assert_eq!(line_index(&lines, 0.5 + 1e-9), Some(1));
assert_eq!(line_index(&lines, 0.51), None);
assert_eq!(line_index(&[], 0.0), None);
}
}