mod conformal;
pub(crate) mod cylindrical;
pub mod dispersive;
mod mode;
pub mod planewave;
pub mod sar;
pub mod sheet;
mod wire;
pub use planewave::PlaneWaveSource;
use crate::axis::Axis;
use crate::engine::boundary::abc::{AbcSheet, AbsorbingSheet, SheetVelocity};
use crate::engine::boundary::cpml::CpmlBoundary;
use crate::engine::boundary::mur::MurBoundary;
use crate::engine::{
ConductingSheet, ConformalOptions, ConformalPec, ConformalReport, Dispersive, EdgeMaterials,
EdgePole, Engine, FaceCondition, FaceConditions, Grid, ObliqueWires, OperatorHandle, SheetEdge,
ThinWire, TimeStep,
};
use crate::fdtd::{Boundary, Cpml, Face, Fdtd};
use crate::{Error, Result};
use csx::discretize::{
DiscretizeOptions, ExcitationField, LumpedEdge, MaterialGrid, SheetInfo, discretize,
};
use csx::properties::{AbcType, DumpMode, DumpType, ExcitationType, ProbeType};
use csx::{CoordSystem, PropertyKind};
#[derive(Debug, Clone, PartialEq)]
pub struct CompileOptions {
pub subsampling: usize,
pub snap_tolerance: f64,
pub update_model: bool,
pub mesh_check: bool,
pub conformal: bool,
pub conformal_options: ConformalOptions,
}
impl CompileOptions {
#[must_use]
pub fn with_conformal(mut self, on: bool) -> Self {
self.conformal = on;
self
}
}
impl Default for CompileOptions {
fn default() -> Self {
let d = DiscretizeOptions::default();
Self {
subsampling: d.subsampling,
snap_tolerance: d.snap_tolerance,
update_model: true,
mesh_check: true,
conformal: false,
conformal_options: ConformalOptions::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NodeMap {
nodes: [usize; 3],
}
impl NodeMap {
pub fn new(nodes: [usize; 3]) -> Self {
Self { nodes }
}
pub fn nodes(&self) -> [usize; 3] {
self.nodes
}
#[inline]
pub fn csx_node(&self, idx: usize) -> [usize; 3] {
let n = self.nodes;
let i = idx % n[0];
let r = idx / n[0];
[i, r % n[1], r / n[1]]
}
#[inline]
pub fn csx_index(&self, [i, j, k]: [usize; 3]) -> usize {
let n = self.nodes;
i + n[0] * (j + n[1] * k)
}
#[inline]
pub fn engine_index(&self, [i, j, k]: [usize; 3]) -> usize {
let n = self.nodes;
(i * n[1] + j) * n[2] + k
}
pub fn edge(&self, e: csx::discretize::Edge) -> (Axis, [usize; 3]) {
let axis = Axis::from_index(e.dir).expect("csx edge direction in 0..3");
(axis, self.csx_node(e.index))
}
pub fn reorder<T: Copy + Default>(&self, csx: &[T]) -> Vec<T> {
let n = self.nodes;
let mut out = vec![T::default(); n[0] * n[1] * n[2]];
let mut e = 0;
for i in 0..n[0] {
for j in 0..n[1] {
let base = i + n[0] * j;
let stride = n[0] * n[1];
for (k, o) in out[e..e + n[2]].iter_mut().enumerate() {
*o = csx[base + stride * k];
}
e += n[2];
}
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ModelSources {
pub excitations: Vec<ExcitationField>,
pub lumped: Vec<LumpedEdge>,
pub sheets: Vec<SheetInfo>,
pub probes: Vec<ProbeSpec<csx::properties::ProbeBox>>,
pub dumps: Vec<ProbeSpec<csx::properties::DumpBox>>,
pub plane_waves: Vec<PlaneWaveSource>,
pub sar: Vec<sar::SarSpec>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProbeSpec<T> {
pub name: String,
pub id: u32,
pub settings: T,
pub start: [usize; 3],
pub stop: [usize; 3],
pub mode_entries: Vec<(Axis, [usize; 3], f64)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AbcSpec {
pub name: String,
pub sheet: AbcSheet,
pub phase_velocity: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Compiled {
pub grid: Grid,
pub delta_unit: f64,
pub materials: EdgeMaterials,
pub faces: FaceConditions,
pub mur: Vec<(Face, f64)>,
pub pml: Vec<(Face, Cpml)>,
pub timestep: TimeStep,
pub threads: usize,
pub node_map: NodeMap,
pub abc: Vec<AbcSpec>,
pub dispersive: Vec<EdgePole>,
pub thin_wires: Vec<ThinWire>,
pub oblique_wires: Option<ObliqueWires>,
pub sheets: Vec<SheetEdge>,
pub max_frequency: Option<f64>,
pub conformal: Option<ConformalPec>,
pub sources: ModelSources,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct BoundaryHandles {
pub mur: Option<OperatorHandle<MurBoundary>>,
pub dispersive: Option<OperatorHandle<Dispersive>>,
pub cpml: Option<OperatorHandle<CpmlBoundary>>,
pub sheets: Option<OperatorHandle<ConductingSheet>>,
pub abc: Vec<OperatorHandle<AbsorbingSheet>>,
pub oblique_wires: Option<OperatorHandle<ObliqueWires>>,
pub conformal: Option<ConformalReport>,
}
impl Compiled {
pub fn build(&self) -> Result<(Engine, BoundaryHandles)> {
let mut engine = self.builder().build()?;
let h = self.add_boundary_operators(&mut engine)?;
Ok((engine, h))
}
pub(crate) fn builder(&self) -> crate::engine::EngineBuilder {
Engine::builder(self.grid.clone())
.edge_materials(self.materials.clone())
.faces(self.faces)
.timestep(self.timestep)
.threads(self.threads)
}
pub(crate) fn add_boundary_operators(&self, engine: &mut Engine) -> Result<BoundaryHandles> {
let mut h = BoundaryHandles::default();
if let Some(c) = &self.conformal {
h.conformal = Some(c.apply(engine)?);
}
if !self.pml.is_empty() {
let op = CpmlBoundary::new(engine, self.pml.iter().copied())?;
h.cpml = Some(engine.add_operator(op));
}
if !self.mur.is_empty() {
let op = MurBoundary::with_faces(engine, &self.mur)?;
h.mur = Some(engine.add_operator(op));
}
if !self.dispersive.is_empty() {
let mut d = Dispersive::new();
for &e in &self.dispersive {
d.push_entry(e);
}
h.dispersive = Some(d.add_to(engine)?);
}
if !self.sheets.is_empty() {
let mut s = ConductingSheet::new(self.max_frequency);
for &e in &self.sheets {
s.push(e);
}
h.sheets = Some(s.add_to(engine)?);
}
if let Some(w) = &self.oblique_wires {
h.oblique_wires = Some(engine.add_operator(w.clone()));
}
for a in &self.abc {
let v = match a.phase_velocity {
Some(v) => SheetVelocity::Fixed(v),
None => SheetVelocity::Local(&self.materials),
};
let op = AbsorbingSheet::new(engine, &a.sheet, v)
.map_err(|e| Error::Invalid(format!("AbsorbingBC {:?}: {e}", a.name)))?;
h.abc.push(engine.add_operator(op));
}
Ok(h)
}
}
pub fn grid_from_csx(g: &csx::RectilinearGrid) -> Result<Grid> {
let unit = g.delta_unit;
if !(unit.is_finite() && unit > 0.0) {
return Err(Error::Invalid(format!(
"grid delta unit {unit} must be finite and > 0"
)));
}
if g.coord_system == CoordSystem::Cylindrical {
let scale = |a: usize, s: f64| g.lines(a).iter().map(|&x| x * s).collect();
return Grid::cylindrical([scale(0, unit), scale(1, 1.0), scale(2, unit)]);
}
Grid::new(core::array::from_fn(|a| {
g.lines(a).iter().map(|&x| x * unit).collect()
}))
}
type AbsorbingFaces = (Vec<(Face, f64)>, Vec<(Face, Cpml)>);
fn map_boundaries(fdtd: &Fdtd) -> (FaceConditions, AbsorbingFaces) {
let mut faces = FaceConditions::default();
let (mut mur, mut pml) = (Vec::new(), Vec::new());
for (face, b) in fdtd.get_boundaries().iter() {
faces[face] = match b {
Boundary::Pec => FaceCondition::Pec,
Boundary::Pmc => FaceCondition::Pmc,
Boundary::Periodic => FaceCondition::Periodic,
Boundary::Mur(m) => {
mur.push((face, m.velocity()));
FaceCondition::Pec
}
Boundary::Pml(p) => {
pml.push((face, *p));
FaceCondition::Pec
}
};
}
(faces, (mur, pml))
}
fn property_warnings(model: &csx::ContinuousStructure, out: &mut Vec<String>) {
for p in model.properties() {
let what = match &p.kind {
PropertyKind::Excitation(e) if !e.enabled => "disabled; ignored",
PropertyKind::Excitation(e)
if !e.excitation_type.is_electric()
&& !e.excitation_type.is_magnetic()
&& e.excitation_type != ExcitationType::PlaneWave =>
{
"unknown excitation type; ignored"
}
PropertyKind::ProbeBox(pb) => match pb.probe_type {
ProbeType::Voltage
| ProbeType::Current
| ProbeType::EField
| ProbeType::HField
| ProbeType::WaveguideVoltage
| ProbeType::WaveguideCurrent => continue,
_ => "unknown probe type; ignored",
},
PropertyKind::DumpBox(d) => match d.dump_type {
DumpType::EField | DumpType::HField => continue,
DumpType::EFieldFd
| DumpType::HFieldFd
| DumpType::SarLocal
| DumpType::Sar1g
| DumpType::Sar10g
| DumpType::SarRaw
if d.probe.frequencies.is_empty() =>
{
"frequency-domain dump without frequencies; ignored"
}
DumpType::EFieldFd
| DumpType::HFieldFd
| DumpType::SarLocal
| DumpType::Sar1g
| DumpType::Sar10g
| DumpType::SarRaw => continue,
_ => {
"only E/H field dumps (time or frequency domain) and SAR dumps are \
supported; ignored"
}
},
_ => continue,
};
out.push(format!(
"property {:?} ({}): {what}",
p.name,
p.kind.type_name()
));
}
}
type Specs = (
Vec<ProbeSpec<csx::properties::ProbeBox>>,
Vec<ProbeSpec<csx::properties::DumpBox>>,
);
fn probe_specs(model: &csx::ContinuousStructure, grid: &Grid, warnings: &mut Vec<String>) -> Specs {
let (mut probes, mut dumps) = (Vec::new(), Vec::new());
for p in model.properties() {
if !matches!(p.kind, PropertyKind::ProbeBox(_) | PropertyKind::DumpBox(_)) {
continue;
}
let Some(prim) = p.primitives.first() else {
warnings.push(format!(
"property {:?} ({}): no primitive; ignored",
p.name,
p.kind.type_name()
));
continue;
};
if p.primitives.len() > 1 {
warnings.push(format!(
"property {:?} ({}): only the first of {} primitives is used",
p.name,
p.kind.type_name(),
p.primitives.len()
));
}
let (start, stop) = snap_primitive(&model.grid, prim, "probe/dump", &p.name, warnings);
match &p.kind {
PropertyKind::ProbeBox(pb) => {
let waveguide = matches!(
pb.probe_type,
ProbeType::WaveguideVoltage | ProbeType::WaveguideCurrent
);
if waveguide && grid.is_cylindrical() {
warnings.push(format!(
"probe {:?}: waveguide mode probes need a Cartesian grid; ignored",
p.name
));
continue;
}
let mode_entries = if waveguide {
match mode::mode_entries(
model,
p,
pb,
grid,
model.grid.delta_unit,
start,
stop,
warnings,
) {
Some(e) => e,
None => continue,
}
} else {
Vec::new()
};
probes.push(ProbeSpec {
name: p.name.clone(),
id: p.id(),
settings: pb.clone(),
start,
stop,
mode_entries,
})
}
PropertyKind::DumpBox(d) => {
if d.dump_mode == DumpMode::NoInterpolation {
warnings.push(format!(
"dump {:?}: NoInterpolation is dumped at the nodes (Node mode)",
p.name
));
}
if d.opt_resolution.is_some() {
warnings.push(format!(
"dump {:?}: OptResolution is not supported; full resolution used",
p.name
));
}
dumps.push(ProbeSpec {
name: p.name.clone(),
id: p.id(),
settings: d.clone(),
start,
stop,
mode_entries: Vec::new(),
})
}
_ => {}
}
}
(probes, dumps)
}
fn snap_primitive(
grid: &csx::RectilinearGrid,
prim: &csx::Primitive,
what: &str,
name: &str,
warnings: &mut Vec<String>,
) -> ([usize; 3], [usize; 3]) {
let gcs = grid.coord_system;
let cart = prim.effective_cs(gcs) == gcs;
let tf = |v: csx::Vec3| {
if prim.transform.is_empty() {
v
} else {
prim.transform.apply(v)
}
};
let (a, b) = match &prim.shape {
csx::Shape::Box(bx) if cart => (tf(bx.start.value()), tf(bx.stop.value())),
csx::Shape::Point(pt) if cart => {
let v = tf(pt.point.value());
(v, v)
}
other => {
warnings.push(format!(
"{what} {name:?}: {} primitive replaced by its bounding box",
other.type_name()
));
let bb = prim.bounding_box_in(gcs, gcs);
(bb.min, bb.max)
}
};
let mut off = false;
let mut outside = false;
let mut snap = |v: f64, d: usize| -> usize {
let lines = grid.lines(d);
if v < lines[0] || v > lines[lines.len() - 1] {
outside = true;
}
if grid.snap(d, v).is_none() {
off = true;
}
grid.nearest_line(d, v).unwrap_or(0)
};
let start = [0, 1, 2].map(|d| snap(a.0[d], d));
let stop = [0, 1, 2].map(|d| snap(b.0[d], d));
if outside {
warnings.push(format!(
"{what} {name:?}: box extends outside the mesh; clamped to the mesh"
));
} else if off {
warnings.push(format!(
"{what} {name:?}: box corners snapped to the nearest mesh lines"
));
}
(start, stop)
}
fn abc_specs(model: &csx::ContinuousStructure, warnings: &mut Vec<String>) -> Vec<AbcSpec> {
let mut out = Vec::new();
for p in model.properties() {
let PropertyKind::AbsorbingBC(abc) = &p.kind else {
continue;
};
if p.primitives.is_empty() {
warnings.push(format!("AbsorbingBC {:?}: no primitive; ignored", p.name));
continue;
}
match abc.abc_type {
AbcType::Mur1st => {}
t => warnings.push(format!(
"AbsorbingBC {:?}: AbsorbingBoundaryType {} not implemented; \
first-order Mur (MUR_1ST) used",
p.name,
t.name().map_or_else(|| t.id().to_string(), str::to_string)
)),
}
let v = abc.phase_velocity.value();
let phase_velocity = if v.is_finite() && v > 0.0 {
Some(v)
} else {
if v != 0.0 {
warnings.push(format!(
"AbsorbingBC {:?}: PhaseVelocity {v} invalid; local medium velocity used",
p.name
));
}
None
};
for prim in &p.primitives {
let (a, b) = snap_primitive(&model.grid, prim, "AbsorbingBC", &p.name, warnings);
let start = [0, 1, 2].map(|d| a[d].min(b[d]));
let stop = [0, 1, 2].map(|d| a[d].max(b[d]));
let flat: Vec<usize> = (0..3).filter(|&d| start[d] == stop[d]).collect();
let &[d] = flat.as_slice() else {
let what = if flat.is_empty() {
"a volume"
} else {
"a line or point"
};
warnings.push(format!(
"AbsorbingBC {:?}: primitive is {what} on the mesh, not a sheet; ignored",
p.name
));
continue;
};
let gcs = model.grid.coord_system;
let bb = prim.bounding_box_in(gcs, gcs);
if bb.max.0[d] - bb.min.0[d] > 0.0 {
warnings.push(format!(
"AbsorbingBC {:?}: primitive has a thickness along {}; flattened onto \
mesh plane {}",
p.name,
["x", "y", "z"][d],
start[d]
));
}
let normal = Axis::from_index(d).expect("axis in 0..3");
out.push(AbcSpec {
name: p.name.clone(),
sheet: AbcSheet {
normal,
plane: start[d],
positive: abc.normal_sign_positive,
start,
stop,
},
phase_velocity,
});
}
}
out
}
pub fn compile(
model: &csx::ContinuousStructure,
fdtd: &Fdtd,
opts: &CompileOptions,
) -> Result<Compiled> {
fdtd.validate()?;
let grid = grid_from_csx(&model.grid)?;
let updated;
let model = if opts.update_model {
let mut m = model.clone();
m.update()?;
updated = m;
&updated
} else {
model
};
let mut warnings: Vec<String> = model.validate().iter().map(ToString::to_string).collect();
let grid = cylindrical::multigrid(grid, &model.grid, fdtd.get_multigrid(), &mut warnings)?;
let (oblique, stripped) = wire::oblique_wires(model, &grid, &mut warnings);
let full_model = model;
let model = stripped.as_ref().unwrap_or(model);
let mut mg: MaterialGrid = discretize(
model,
&DiscretizeOptions::default()
.with_subsampling(opts.subsampling)
.with_threads(fdtd.resolved_threads())
.with_snap_tolerance(opts.snap_tolerance),
)?;
oblique.renumber_warnings(&mut mg.warnings);
let node_map = NodeMap::new(grid.nodes());
if mg.dims != grid.nodes() {
return Err(Error::Invalid(format!(
"discretised grid has {:?} nodes, expected {:?}",
mg.dims,
grid.nodes()
)));
}
let pw_names: Vec<&str> = model
.properties()
.iter()
.filter(|p| {
matches!(&p.kind, PropertyKind::Excitation(e)
if e.excitation_type == ExcitationType::PlaneWave)
})
.map(|p| p.name.as_str())
.collect();
warnings.extend(
mg.warnings
.iter()
.filter(|w| !planewave::is_csx_skip_warning(w, &pw_names))
.cloned(),
);
property_warnings(model, &mut warnings);
let cyl = grid.is_cylindrical();
let mut plane_waves = planewave::plane_wave_sources(model, fdtd, &mut warnings);
if cyl && !plane_waves.is_empty() {
warnings.push(format!(
"{} plane-wave excitation(s) ignored: plane waves need a Cartesian grid",
plane_waves.len()
));
plane_waves.clear();
}
let sheet_map = if cyl {
cylindrical::sheets_as_pec(&mg, &mut warnings)
} else {
sheet::sheet_edges(model, &mg, &node_map, opts.snap_tolerance, &mut warnings)
};
let mut sheet_pec: [Vec<bool>; 3] = core::array::from_fn(|a| vec![false; mg.pec[a].len()]);
for &(d, idx) in &sheet_map.pec {
sheet_pec[d][idx] = true;
}
let mut materials = EdgeMaterials::vacuum(&grid);
for axis in Axis::ALL {
let a = axis.index();
materials
.eps_r_mut(axis)
.copy_from_slice(&node_map.reorder(&mg.epsilon[a]));
materials
.kappa_mut(axis)
.copy_from_slice(&node_map.reorder(&mg.kappa[a]));
materials
.mu_r_mut(axis)
.copy_from_slice(&node_map.reorder(&mg.mue[a]));
materials
.sigma_m_mut(axis)
.copy_from_slice(&node_map.reorder(&mg.sigma[a]));
let pec: Vec<bool> = mg.pec[a]
.iter()
.zip(&sheet_pec[a])
.map(|(&p, &s)| p || s)
.collect();
materials
.pec_mask_mut(axis)
.copy_from_slice(&node_map.reorder(&pec));
}
if opts.conformal && cyl {
warnings.push(
"conformal PEC is not available on cylindrical grids; using the staircase".into(),
);
}
let conformal = if opts.conformal && !cyl {
let pec: [&[bool]; 3] = [
materials.pec_mask(Axis::X),
materials.pec_mask(Axis::Y),
materials.pec_mask(Axis::Z),
];
let threads = fdtd.resolved_threads();
let geo = conformal::geometry(model, &pec, opts.snap_tolerance, threads, &mut warnings);
for &(axis, k, v) in &geo.pec {
materials.pec_mask_mut(axis)[k] = v;
}
Some(geo.conformal.with_options(opts.conformal_options))
} else {
None
};
let dispersive = if cyl {
cylindrical::no_dispersive(model, &mut warnings);
Vec::new()
} else {
dispersive::dispersive_poles(model, &node_map, opts.subsampling, &mut warnings)
};
let thin_wires = wire::thin_wires(
full_model,
&grid,
&mg.lumped,
opts.snap_tolerance,
&mut materials,
&mut warnings,
);
planewave::set_backgrounds(&mut plane_waves, &grid, &materials)?;
let (faces, (mut mur, mut pml)) = map_boundaries(fdtd);
if cyl {
cylindrical::drop_ignored_boundaries(&grid, &mut mur, &mut pml, &mut warnings);
}
sheet::pml_warning(&sheet_map.edges, grid.nodes(), &pml, &mut warnings);
let oblique_wires = wire::oblique_operator(
&oblique,
&grid,
&materials,
&mg.lumped,
&faces,
&mut warnings,
);
let timestep = match fdtd.get_timestep() {
Some(dt) => TimeStep::Fixed(dt),
None => TimeStep::Courant(fdtd.get_timestep_factor()),
};
let MaterialGrid {
mut excitations,
lumped,
sheets,
..
} = mg;
excitations.retain(|f| {
model
.property_by_name(&f.name)
.is_none_or(|p| !matches!(&p.kind, PropertyKind::Excitation(e) if !e.enabled))
});
cylindrical::multigrid_edge_warnings(&grid, &node_map, &excitations, &lumped, &mut warnings);
let (probes, mut dumps) = probe_specs(model, &grid, &mut warnings);
let sar = if cyl {
cylindrical::no_sar(&mut dumps, &mut warnings);
Vec::new()
} else {
sar::sar_specs(model, &dumps, opts.subsampling, &mut warnings)
};
let abc = abc_specs(model, &mut warnings);
Ok(Compiled {
grid,
delta_unit: model.grid.delta_unit,
materials,
faces,
mur,
pml,
timestep,
threads: fdtd.get_threads().unwrap_or(0),
node_map,
abc,
dispersive,
thin_wires,
oblique_wires,
sheets: sheet_map.edges,
max_frequency: fdtd.get_max_frequency(),
conformal,
sources: ModelSources {
excitations,
lumped,
sheets,
probes,
dumps,
plane_waves,
sar,
},
warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_map_round_trip() {
let m = NodeMap::new([3, 4, 5]);
let csx: Vec<usize> = (0..60).collect();
let e = m.reorder(&csx);
for i in 0..3 {
for j in 0..4 {
for k in 0..5 {
let n = [i, j, k];
assert_eq!(m.csx_node(m.csx_index(n)), n);
assert_eq!(e[m.engine_index(n)], m.csx_index(n));
}
}
}
let (a, n) = m.edge(csx::discretize::Edge {
dir: 2,
index: m.csx_index([1, 2, 3]),
});
assert_eq!((a, n), (Axis::Z, [1, 2, 3]));
}
}