use csx::expr::{CompiledExpr, Expr};
use csx::properties::{ProbeBox, ProbeType, WEIGHT_VARS, weight_vars};
use crate::axis::Axis;
use crate::engine::Grid;
pub(crate) type Entry = (Axis, [usize; 3], f64);
#[allow(clippy::too_many_arguments)]
pub(super) fn mode_entries(
model: &csx::ContinuousStructure,
prop: &csx::Property,
pb: &ProbeBox,
grid: &Grid,
delta_unit: f64,
start: [usize; 3],
stop: [usize; 3],
warnings: &mut Vec<String>,
) -> Option<Vec<Entry>> {
let name = &prop.name;
let mut funcs: Vec<CompiledExpr> = Vec::with_capacity(3);
let mut any = false;
for c in ["X", "Y", "Z"] {
let key = format!("ModeFunction{c}");
let src = prop.attribute(&key).unwrap_or("0");
any |= prop.attribute(&key).is_some();
let f = Expr::parse(src).and_then(|e| e.compile(&WEIGHT_VARS, |n| model.parameters.get(n)));
match f {
Ok(f) => funcs.push(f),
Err(e) => {
warnings.push(format!(
"waveguide probe {name:?}: invalid {key} {src:?}: {e}; ignored"
));
return None;
}
}
}
if !any {
warnings.push(format!(
"waveguide probe {name:?}: no ModeFunctionX/Y/Z attributes; it reads 0"
));
}
let flat: Vec<usize> = (0..3).filter(|&d| start[d] == stop[d]).collect();
let normal = match pb.norm_dir {
Some(d) => Axis::from_index(d.index()).expect("axis index in 0..3"),
None if flat.len() == 1 => Axis::from_index(flat[0]).expect("axis index in 0..3"),
None => {
warnings.push(format!(
"waveguide probe {name:?}: no normal direction (set NormDir or use a box \
that is flat along one axis); ignored"
));
return None;
}
};
let n = normal.index();
if start[n] != stop[n] {
warnings.push(format!(
"waveguide probe {name:?}: box is not flat along its normal {normal}; the \
plane at its start is used"
));
}
let k = start[n];
let lo = [0, 1, 2].map(|d| start[d].min(stop[d]));
let hi = [0, 1, 2].map(|d| start[d].max(stop[d]));
let nodes = grid.nodes();
let lines = |a: usize| grid.lines(Axis::from_index(a).expect("axis index in 0..3"));
let eval = |c: usize, p: [f64; 3]| -> f64 {
let vars = weight_vars(csx::Vec3::new(
p[0] / delta_unit,
p[1] / delta_unit,
p[2] / delta_unit,
));
funcs[c].eval(&vars)
};
let clipped = |a: usize, i: usize| -> f64 {
let l = lines(a);
let left = if i > lo[a] {
0.5 * (l[i] - l[i - 1])
} else {
0.0
};
let right = if i < hi[a] {
0.5 * (l[i + 1] - l[i])
} else {
0.0
};
left + right
};
let (ta, tb) = normal.others();
let mut out = Vec::new();
match pb.probe_type {
ProbeType::WaveguideVoltage => {
for (t, o) in [(ta, tb), (tb, ta)] {
let (ti, oi) = (t.index(), o.index());
for it in lo[ti]..hi[ti] {
for io in lo[oi]..=hi[oi] {
let mut node = [0; 3];
node[n] = k;
node[ti] = it;
node[oi] = io;
let mut p = [0.0; 3];
p[n] = lines(n)[k];
p[ti] = 0.5 * (lines(ti)[it] + lines(ti)[it + 1]);
p[oi] = lines(oi)[io];
let w = eval(ti, p) * clipped(oi, io);
if w != 0.0 && w.is_finite() {
out.push((t, node, w));
}
}
}
}
}
ProbeType::WaveguideCurrent => {
let ln = lines(n);
let below = (k > 0).then(|| (k - 1, 0.5 * (ln[k] - ln[k - 1])));
let above = (k + 1 < nodes[n]).then(|| (k, 0.5 * (ln[k + 1] - ln[k])));
let planes: Vec<(usize, f64)> = match (below, above) {
(Some((kb, db)), Some((ka, da))) => {
vec![(kb, da / (da + db)), (ka, db / (da + db))]
}
(Some((kb, _)), None) => vec![(kb, 1.0)],
(None, Some((ka, _))) => vec![(ka, 1.0)],
(None, None) => Vec::new(),
};
for (t, o) in [(ta, tb), (tb, ta)] {
let (ti, oi) = (t.index(), o.index());
let ta_ = Axis::from_index(ti).expect("axis index in 0..3");
for it in lo[ti]..=hi[ti] {
let dual = grid.dual_delta(ta_, it);
if dual <= 0.0 {
continue;
}
for io in lo[oi]..hi[oi] {
let mut p = [0.0; 3];
p[n] = ln[k];
p[ti] = lines(ti)[it];
p[oi] = 0.5 * (lines(oi)[io] + lines(oi)[io + 1]);
let d_o = lines(oi)[io + 1] - lines(oi)[io];
let base = eval(ti, p) * clipped(ti, it) * d_o / dual;
if base == 0.0 || !base.is_finite() {
continue;
}
for &(kp, wp) in &planes {
let mut node = [0; 3];
node[n] = kp;
node[ti] = it;
node[oi] = io;
out.push((t, node, base * wp));
}
}
}
}
}
_ => return None,
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use csx::primitives::BoxShape;
use csx::{ContinuousStructure, Primitive, Property};
fn setup(ty: ProbeType, fx: &str, fy: &str) -> (Vec<Entry>, Grid) {
let mut m = ContinuousStructure::new();
m.grid.delta_unit = 1e-3;
m.grid.set_lines(0, [0.0, 1.0, 3.0, 4.0]).unwrap();
m.grid.set_lines(1, [0.0, 2.0, 3.0]).unwrap();
m.grid.set_lines(2, [0.0, 1.0, 3.0]).unwrap();
let mut p = Property::probe("p", ProbeBox::new(ty));
p.set_attribute("ModeFunctionX", fx);
p.set_attribute("ModeFunctionY", fy);
let id = m.add_property(p);
m.add_primitive(
id,
Primitive::new(BoxShape::new([0., 0., 1.], [4., 3., 1.])),
)
.unwrap();
let grid = Grid::new([0, 1, 2].map(|a| m.grid.lines(a).iter().map(|x| x * 1e-3).collect()))
.unwrap();
let prop = &m.properties()[0];
let csx::PropertyKind::ProbeBox(pb) = &prop.kind else {
unreachable!()
};
let mut w = Vec::new();
let e = mode_entries(&m, prop, pb, &grid, 1e-3, [0, 0, 1], [3, 2, 1], &mut w).unwrap();
assert!(w.is_empty(), "{w:?}");
(e, grid)
}
#[test]
fn voltage_of_uniform_field() {
let (e, g) = setup(ProbeType::WaveguideVoltage, "1", "0");
let s: f64 = e
.iter()
.map(|&(a, n, w)| {
assert_eq!(a, Axis::X);
assert_eq!(n[2], 1);
w * g.delta(a, n[0])
})
.sum();
assert!((s - 12e-6).abs() < 1e-18, "{s}");
}
#[test]
fn current_of_uniform_field() {
let (e, g) = setup(ProbeType::WaveguideCurrent, "0", "1");
let mut planes = [0.0; 2];
let s: f64 = e
.iter()
.map(|&(a, n, w)| {
assert_eq!(a, Axis::Y);
planes[n[2]] += w * g.dual_delta(a, n[1]);
w * g.dual_delta(a, n[1])
})
.sum();
assert!((s - 12e-6).abs() < 1e-18, "{s}");
assert!((planes[0] / s - 2.0 / 3.0).abs() < 1e-12, "{planes:?}");
}
#[test]
fn mode_function_positions() {
let (e, _) = setup(ProbeType::WaveguideVoltage, "y", "0");
for &(_, n, w) in &e {
let y = [0.0, 2.0, 3.0][n[1]];
let wo = [1.0, 1.5, 0.5][n[1]] * 1e-3;
assert!((w - y * wo).abs() < 1e-15);
}
assert_eq!(e.len(), 3 * 2); }
}