mod bloch;
mod map;
mod results;
#[cfg(feature = "xml")]
mod xml;
pub use results::{
DumpData, DumpResult, IncidentWave, ProbeKind, ProbeResult, ProbeValues, Results, SarResult,
};
use crate::Result;
use crate::compile::{BoundaryHandles, CompileOptions, Compiled, ModelSources, NodeMap, compile};
use crate::engine::boundary::periodic::{BlochCoupling, BlochStepper};
use crate::engine::run::drive;
use crate::engine::{
BlochPair, EdgeSource, Engine, FreqDump, Grid, LumpedElement, OperatorHandle,
PeriodicPlaneWave, PlaneWave, Progress, RunLimits, RunOutcome, TimeDump, WavePart,
};
use crate::fdtd::Fdtd;
use crate::nf2ff::{Nf2ffBox, Nf2ffHandle, Nf2ffResult};
use crate::post::sar::SarVolume;
use map::{BlochMap, DumpOp, Mapped, ProbeOp};
#[derive(Debug)]
pub struct Simulation {
engine: Engine,
fdtd: Fdtd,
delta_unit: f64,
node_map: NodeMap,
boundaries: BoundaryHandles,
sources: ModelSources,
mapped: Mapped,
warnings: Vec<String>,
bloch: Option<Box<BlochState>>,
}
#[derive(Debug)]
struct BlochState {
imag: Engine,
coupling: BlochCoupling,
mapped: Mapped,
k: [f64; 3],
}
impl Simulation {
pub fn new(model: &csx::ContinuousStructure, fdtd: &Fdtd) -> Result<Self> {
Self::with_options(model, fdtd, &CompileOptions::default())
}
pub fn with_options(
model: &csx::ContinuousStructure,
fdtd: &Fdtd,
opts: &CompileOptions,
) -> Result<Self> {
let compiled = compile(model, fdtd, opts)?;
let k = bloch::wavevector(fdtd, &compiled.sources.plane_waves, compiled.grid.nodes())?;
let (mut engine, boundaries, imag) = match k {
None => {
let (e, h) = compiled.build()?;
(e, h, None)
}
Some(k) => {
let psi = BlochPair::phase_for(&compiled.grid, k);
let (coupling, b) = BlochCoupling::new(compiled.builder(), psi)?;
let mut re = b.clone().build()?;
let mut im = b.build()?;
let h = compiled.add_boundary_operators(&mut re)?;
compiled.add_boundary_operators(&mut im)?;
(re, h, Some((im, coupling, k)))
}
};
let mut warnings = compiled.warnings.clone();
if let Some(w) = boundaries.conformal.as_ref().and_then(|r| r.warning()) {
warnings.push(w);
}
if opts.mesh_check {
warnings.extend(mesh_warnings(model, fdtd, &compiled, opts)?);
}
let part = |part| k.map(|k| BlochMap { part, k });
let mut mapped = map::map_sources(
&mut engine,
&compiled.sources,
&compiled.node_map,
fdtd,
&mut warnings,
part(WavePart::Real),
)?;
mapped.lumped_ids = mapped
.lumped
.iter()
.map(|(n, _)| model.property_id(n).map_or(u32::MAX, |p| p.get()))
.collect();
let bloch = match imag {
None => None,
Some((mut imag, coupling, k)) => {
let mut ignored = Vec::new();
let mapped = map::map_sources(
&mut imag,
&compiled.sources,
&compiled.node_map,
fdtd,
&mut ignored,
part(WavePart::Imag),
)?;
Some(Box::new(BlochState {
imag,
coupling,
mapped,
k,
}))
}
};
Ok(Self {
engine,
fdtd: fdtd.clone(),
delta_unit: compiled.delta_unit,
node_map: compiled.node_map,
boundaries,
sources: compiled.sources,
mapped,
warnings,
bloch,
})
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub fn engine_mut(&mut self) -> &mut Engine {
&mut self.engine
}
pub fn into_engine(self) -> Engine {
self.engine
}
pub fn grid(&self) -> &Grid {
self.engine.grid()
}
pub fn delta_unit(&self) -> f64 {
self.delta_unit
}
pub fn fdtd(&self) -> &Fdtd {
&self.fdtd
}
pub fn node_map(&self) -> &NodeMap {
&self.node_map
}
pub fn boundaries(&self) -> &BoundaryHandles {
&self.boundaries
}
pub fn sources(&self) -> &ModelSources {
&self.sources
}
pub fn warnings(&self) -> &[String] {
&self.warnings
}
pub fn lumped_element(&self, name: &str) -> Option<&LumpedElement> {
let h = find(&self.mapped.lumped, name)?;
Some(self.engine.operator(&h))
}
pub fn source(&self, name: &str) -> Option<&EdgeSource> {
let h = find(&self.mapped.sources, name)?;
Some(self.engine.operator(&h))
}
pub fn plane_wave(&self, name: &str) -> Option<&PlaneWave> {
let h = find(&self.mapped.plane_waves, name)?;
Some(self.engine.operator(&h))
}
pub fn periodic_plane_wave(&self, name: &str) -> Option<&PeriodicPlaneWave> {
let h = find(&self.mapped.periodic_waves, name)?;
Some(self.engine.operator(&h))
}
pub fn is_bloch(&self) -> bool {
self.bloch.is_some()
}
pub fn bloch_wavevector(&self) -> Option<[f64; 3]> {
self.bloch.as_ref().map(|b| b.k)
}
pub fn bloch_phase(&self) -> Option<[f64; 3]> {
self.bloch.as_ref().map(|b| b.coupling.phase())
}
pub fn imag_engine(&self) -> Option<&Engine> {
self.bloch.as_ref().map(|b| &b.imag)
}
pub fn time_dump(&self, name: &str) -> Option<&TimeDump> {
match self.mapped.dumps.iter().find(|d| d.name == name)?.op {
DumpOp::Time(h) => Some(self.engine.operator(&h)),
DumpOp::Freq(_) => None,
}
}
pub fn freq_dump(&self, name: &str) -> Option<&FreqDump> {
match self.mapped.dumps.iter().find(|d| d.name == name)?.op {
DumpOp::Freq(h) => Some(self.engine.operator(&h)),
DumpOp::Time(_) => None,
}
}
pub fn add_nf2ff(&mut self, b: &Nf2ffBox) -> Result<Nf2ffHandle> {
if self.bloch.is_some() {
return Err(crate::Error::Unsupported(
"NF2FF boxes in a Bloch run (periodic structures radiate Floquet modes, \
not a far field)"
.into(),
));
}
let k = map::dump_interval(&self.fdtd, self.engine.dt());
let (op, mirrors) = b.build(self.engine.grid(), self.delta_unit, k)?;
let n = self.engine.grid().nodes();
for (face, bc) in self.fdtd.get_boundaries().iter() {
let cells = bc.cells();
if cells == 0 {
continue;
}
let a = face.axis().index();
let inside = if face.is_max() {
op.hi()[a] + cells >= n[a] - 1
} else {
op.lo()[a] <= cells
};
if inside {
self.warnings.push(format!(
"NF2FF box reaches into the PML of {face:?}; keep it inside the \
interior"
));
}
}
let mut op = op;
if b.get_background().is_none() {
let mut faces = [false; 6];
for f in op.faces() {
faces[f.index()] = true;
}
let co = self.engine.coefficients();
let media = crate::engine::medium::CoefficientMedia {
grid: self.engine.grid(),
co: &co,
dt: self.engine.dt(),
};
let found = crate::engine::medium::surface_medium(
self.engine.grid(),
&media,
op.lo(),
op.hi(),
faces,
);
drop(co);
match found {
Ok(Some(m)) if !m.is_lossless() => {
return Err(crate::Error::Unsupported(format!(
"NF2FF box in a lossy background (kappa = {:.4e} S/m, sigma_m = {:.4e} \
Ohm/m): there is no far field in a lossy medium; set \
Nf2ffBox::background to use the lossless part",
m.kappa, m.sigma_m
)));
}
Ok(Some(m)) if (m.eps_r - 1.0).abs() > 1e-6 || (m.mu_r - 1.0).abs() > 1e-6 => {
op = op.with_background(m.eps_r, m.mu_r)?;
}
Ok(_) => {}
Err(e) => self.warnings.push(format!(
"NF2FF box: {e}; the transform assumes vacuum (keep the faces in one \
homogeneous medium, or set Nf2ffBox::background)"
)),
}
}
let op = self.engine.add_operator(op);
Ok(Nf2ffHandle { op, mirrors })
}
pub fn nf2ff(&self, h: &Nf2ffHandle) -> Nf2ffResult {
let mut r = self.engine.operator(&h.op).result();
for &m in &h.mirrors {
r = r
.with_mirror(m)
.expect("mirror planes were validated when the box was added");
}
r
}
pub fn run_limits(&self) -> RunLimits {
let end = self.fdtd.get_waveform().map(|w| {
let d = w.duration().unwrap_or(f64::INFINITY);
self.mapped.plane_waves.iter().fold(d, |end, (name, h)| {
let delay = self
.sources
.plane_waves
.iter()
.filter(|p| &p.name == name)
.fold(0.0f64, |m, p| m.max(p.delay));
end.max(delay + d + self.engine.operator(h).transit_time())
})
});
RunLimits::new(self.fdtd.timestep_limit(self.engine.dt()))
.end_criteria(self.fdtd.get_end_criteria().copied())
.excitation_end(end)
}
pub fn run(&mut self) -> Result<Results> {
self.run_with(|_| core::ops::ControlFlow::Continue(()))
}
pub fn run_with<F>(&mut self, progress: F) -> Result<Results>
where
F: FnMut(&Progress) -> core::ops::ControlFlow<()>,
{
let limits = self.run_limits();
let outcome = self.run_engines(&limits, progress)?;
let mut r = self.results();
r.outcome = Some(outcome);
Ok(r)
}
fn run_engines<F>(&mut self, limits: &RunLimits, progress: F) -> Result<RunOutcome>
where
F: FnMut(&Progress) -> core::ops::ControlFlow<()>,
{
match &mut self.bloch {
None => self.engine.run_until_with(limits, progress),
Some(b) => {
let mut s = BlochStepper {
re: &mut self.engine,
im: &mut b.imag,
coupling: &b.coupling,
};
drive(&mut s, limits, progress)
}
}
}
pub fn step(&mut self, n: u64) {
match &mut self.bloch {
None => self.engine.run(n),
Some(b) => {
for _ in 0..n {
b.coupling.step(&mut self.engine, &mut b.imag, false);
}
}
}
}
pub fn results(&self) -> Results {
let dt = self.engine.dt();
let imag = self.bloch.as_deref().map(|b| (&b.imag, &b.mapped));
let probes = self
.mapped
.probes
.iter()
.enumerate()
.map(|(q, p)| {
let (times, values) = probe_series(&self.engine, p);
let eps = 1e-9 * dt;
let lo = times.partition_point(|&t| t < p.start_time - eps);
let hi = if p.stop_time > 0.0 {
times.partition_point(|&t| t <= p.stop_time + eps)
} else {
times.len()
}
.max(lo);
let window = |v: results::ProbeValues| match v {
results::ProbeValues::Scalar(v) => {
results::ProbeValues::Scalar(v[lo..hi].to_vec())
}
results::ProbeValues::Vector(v) => {
results::ProbeValues::Vector(v[lo..hi].to_vec())
}
};
let imag = imag.map(|(e, m)| window(probe_series(e, &m.probes[q]).1));
ProbeResult {
name: p.name.clone(),
id: p.id,
number: p.number,
kind: p.kind,
frequencies: p.frequencies.clone(),
dt,
times: times[lo..hi].to_vec(),
values: window(values),
imag,
}
})
.collect();
let lumped = self
.mapped
.lumped
.iter()
.zip(&self.mapped.lumped_ids)
.enumerate()
.map(|(q, ((name, h), &id))| {
let op = self.engine.operator(h);
let imag = imag.map(|(e, m)| {
results::ProbeValues::Scalar(e.operator(&m.lumped[q].1).current().to_vec())
});
ProbeResult {
name: name.clone(),
id,
number: 0,
kind: ProbeKind::Current,
frequencies: Vec::new(),
dt,
times: op.current_times(),
values: results::ProbeValues::Scalar(op.current().to_vec()),
imag,
}
})
.collect();
let dumps = self
.mapped
.dumps
.iter()
.enumerate()
.map(|(q, d)| {
let other = imag.map(|(e, m)| (e, m.dumps[q].op));
let (data, imag) = match d.op {
DumpOp::Time(h) => {
let op = self.engine.operator(&h);
let imag = match other {
Some((e, DumpOp::Time(hi))) => {
Some(e.operator(&hi).sink().snapshots().to_vec())
}
_ => None,
};
let data = DumpData::Time {
sampler: op.sampler().clone(),
snapshots: op.sink().snapshots().to_vec(),
};
(data, imag)
}
DumpOp::Freq(h) => {
let mut f = self.engine.operator(&h).clone();
if let Some((e, DumpOp::Freq(hi))) = other {
f.add_imaginary(e.operator(&hi));
}
(DumpData::Freq(f), None)
}
};
DumpResult {
name: d.name.clone(),
id: d.id,
file_type: d.file_type,
data,
imag,
}
})
.collect();
let sar = self
.mapped
.sar
.iter()
.map(|e| {
let spec = &self.sources.sar[e.spec];
let op = self.engine.operator(&e.op);
let volumes = (0..spec.frequencies.len())
.map(|k| {
SarVolume::from_edge_dft(
self.engine.grid(),
op,
k,
&spec.sigma,
spec.density.clone(),
)
.expect("SAR specs are validated at compile time")
})
.collect();
SarResult {
name: spec.name.clone(),
id: spec.id,
kind: spec.kind,
file_type: spec.file_type,
frequencies: spec.frequencies.clone(),
volumes,
}
})
.collect();
let incident = self
.mapped
.periodic_waves
.iter()
.map(|(name, h)| {
let op = self.engine.operator(h);
IncidentWave {
name: name.clone(),
dt,
k_t: op.spec().k_t,
e: op.incident().to_vec(),
}
})
.collect();
Results {
outcome: None,
dt,
probes,
lumped,
dumps,
sar,
incident,
}
}
}
fn probe_series(engine: &Engine, p: &map::ProbeEntry) -> (Vec<f64>, results::ProbeValues) {
match p.op {
ProbeOp::Scalar(h) => {
let op = engine.operator(&h);
(
op.times().to_vec(),
results::ProbeValues::Scalar(op.values().to_vec()),
)
}
ProbeOp::Field(h) => {
let op = engine.operator(&h);
let w = p.weight;
let v = op.values().iter().map(|v| v.map(|x| x * w)).collect();
(op.times().to_vec(), results::ProbeValues::Vector(v))
}
}
}
fn mesh_warnings(
model: &csx::ContinuousStructure,
fdtd: &Fdtd,
compiled: &Compiled,
opts: &CompileOptions,
) -> Result<Vec<String>> {
use crate::mesh::{CheckOptions, Severity, check_compiled};
let check = CheckOptions {
snap_tolerance: opts.snap_tolerance,
update_model: opts.update_model,
..CheckOptions::default()
};
let report = check_compiled(model, fdtd, compiled, &check)?;
Ok(report
.issues
.iter()
.filter(|i| i.severity >= Severity::Warning)
.filter(|i| {
!i.message
.starts_with(crate::mesh::CSX_DISCRETISATION_PREFIX)
})
.map(|i| format!("mesh {i}"))
.collect())
}
fn find<T>(list: &[(String, OperatorHandle<T>)], name: &str) -> Option<OperatorHandle<T>> {
list.iter().find(|(n, _)| n == name).map(|(_, h)| *h)
}