pub mod dispersive;
use crate::axis::Axis;
use crate::engine::boundary::cpml::CpmlBoundary;
use crate::engine::boundary::mur::MurBoundary;
use crate::engine::{
Dispersive, EdgeMaterials, EdgePole, Engine, FaceCondition, FaceConditions, Grid,
OperatorHandle, TimeStep,
};
use crate::fdtd::{Boundary, Cpml, Face, Fdtd};
use crate::{Error, Result};
use csx::discretize::{
DiscretizeOptions, ExcitationField, LumpedEdge, MaterialGrid, SheetInfo, discretize,
};
use csx::properties::{DumpMode, DumpType, 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,
}
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,
}
}
}
#[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>>,
}
#[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],
}
#[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 dispersive: Vec<EdgePole>,
pub sources: ModelSources,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BoundaryHandles {
pub mur: Option<OperatorHandle<MurBoundary>>,
pub dispersive: Option<OperatorHandle<Dispersive>>,
pub cpml: Option<OperatorHandle<CpmlBoundary>>,
}
impl Compiled {
pub fn build(&self) -> Result<(Engine, BoundaryHandles)> {
let mut engine = Engine::builder(self.grid.clone())
.edge_materials(self.materials.clone())
.faces(self.faces)
.timestep(self.timestep)
.threads(self.threads)
.build()?;
let mut h = BoundaryHandles::default();
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(&mut engine)?);
}
Ok((engine, h))
}
}
pub fn grid_from_csx(g: &csx::RectilinearGrid) -> Result<Grid> {
if g.coord_system != CoordSystem::Cartesian {
return Err(Error::Unsupported(
"cylindrical grids are not supported yet (milestone M4)".into(),
));
}
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"
)));
}
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::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::AbsorbingBC(_) => {
"AbsorbingBC sheets are not supported yet (M2); ignored"
}
PropertyKind::Excitation(e) if !e.enabled => "disabled; ignored",
PropertyKind::Excitation(e)
if !e.excitation_type.is_electric() && !e.excitation_type.is_magnetic() =>
{
"only E- and H-field excitations are supported (plane waves: later); ignored"
}
PropertyKind::ProbeBox(pb) => match pb.probe_type {
ProbeType::Voltage | ProbeType::Current | ProbeType::EField | ProbeType::HField => {
continue;
}
_ => "waveguide probes are not supported yet (M2); ignored",
},
PropertyKind::DumpBox(d) => match d.dump_type {
DumpType::EField | DumpType::HField => continue,
DumpType::EFieldFd | DumpType::HFieldFd if d.probe.frequencies.is_empty() => {
"frequency-domain dump without frequencies; ignored"
}
DumpType::EFieldFd | DumpType::HFieldFd => continue,
_ => "only E/H field dumps (time or frequency domain) 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, 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, &p.name, warnings);
match &p.kind {
PropertyKind::ProbeBox(pb) => probes.push(ProbeSpec {
name: p.name.clone(),
id: p.id(),
settings: pb.clone(),
start,
stop,
}),
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,
})
}
_ => {}
}
}
(probes, dumps)
}
fn snap_primitive(
grid: &csx::RectilinearGrid,
prim: &csx::Primitive,
name: &str,
warnings: &mut Vec<String>,
) -> ([usize; 3], [usize; 3]) {
let cart = prim
.coord_system
.is_none_or(|c| c == CoordSystem::Cartesian);
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!(
"probe/dump {name:?}: {} primitive replaced by its bounding box",
other.type_name()
));
let bb = prim.bounding_box(CoordSystem::Cartesian);
(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!(
"probe/dump {name:?}: box extends outside the mesh; clamped to the mesh"
));
} else if off {
warnings.push(format!(
"probe/dump {name:?}: box corners snapped to the nearest mesh lines"
));
}
(start, stop)
}
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 mg: MaterialGrid = discretize(
model,
&DiscretizeOptions::default()
.with_subsampling(opts.subsampling)
.with_threads(fdtd.resolved_threads())
.with_snap_tolerance(opts.snap_tolerance),
)?;
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()
)));
}
warnings.extend(mg.warnings.iter().cloned());
property_warnings(model, &mut warnings);
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 mut pec: Vec<bool> = mg.pec[a]
.iter()
.zip(&mg.sheet[a])
.map(|(&p, &s)| p || s != 0)
.collect();
pec = node_map.reorder(&pec);
materials.pec_mask_mut(axis).copy_from_slice(&pec);
}
for s in &mg.sheets {
warnings.push(format!(
"ConductingSheet {:?} (conductivity {} S/m, thickness {} m) approximated as PEC (M2)",
s.name, s.conductivity, s.thickness
));
}
let dispersive =
dispersive::dispersive_poles(model, &node_map, opts.subsampling, &mut warnings);
let (faces, (mur, pml)) = map_boundaries(fdtd);
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))
});
let (probes, dumps) = probe_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,
dispersive,
sources: ModelSources {
excitations,
lumped,
sheets,
probes,
dumps,
},
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]));
}
}