use core::f64::consts::PI;
use crate::error::{Error, Result};
use crate::expr::Expr;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum ExcitationSignal {
Gaussian {
f0: f64,
fc: f64,
},
Sinus {
f0: f64,
},
Dirac {
f_max: f64,
},
Step {
f_max: f64,
},
Custom {
function: String,
f0: f64,
f_max: f64,
},
}
const LN_10: f64 = core::f64::consts::LN_10;
impl ExcitationSignal {
pub fn id(&self) -> i32 {
match self {
ExcitationSignal::Gaussian { .. } => 0,
ExcitationSignal::Sinus { .. } => 1,
ExcitationSignal::Dirac { .. } => 2,
ExcitationSignal::Step { .. } => 3,
ExcitationSignal::Custom { .. } => 10,
}
}
pub fn max_frequency(&self) -> f64 {
match *self {
ExcitationSignal::Gaussian { f0, fc } => f0 + fc,
ExcitationSignal::Sinus { f0 } => f0,
ExcitationSignal::Dirac { f_max } | ExcitationSignal::Step { f_max } => f_max,
ExcitationSignal::Custom { f_max, .. } => f_max,
}
}
pub fn gaussian_width(&self) -> Option<f64> {
match *self {
ExcitationSignal::Gaussian { fc, .. } if fc > 0.0 => Some(LN_10.sqrt() / (PI * fc)),
_ => None,
}
}
pub fn gaussian_delay(&self) -> Option<f64> {
self.gaussian_width().map(|tau| tau * (6.0 * LN_10).sqrt())
}
pub fn duration(&self) -> Option<f64> {
self.gaussian_delay().map(|t0| 2.0 * t0)
}
pub fn evaluator(&self) -> Result<SignalFn> {
Ok(match self {
ExcitationSignal::Custom {
function,
f0,
f_max,
} => {
let (f0, f_max) = (*f0, *f_max);
let e = Expr::parse(function)?.compile(&["t"], |n| match n {
"f0" => Some(f0),
"f_max" | "fmax" => Some(f_max),
_ => None,
})?;
SignalFn(Inner::Custom(e))
}
other => SignalFn(Inner::Builtin(other.clone())),
})
}
pub fn eval(&self, t: f64) -> f64 {
self.evaluator().map_or(f64::NAN, |f| f.eval(t))
}
}
#[derive(Debug, Clone)]
enum Inner {
Builtin(ExcitationSignal),
Custom(crate::expr::CompiledExpr),
}
#[derive(Debug, Clone)]
pub struct SignalFn(Inner);
impl SignalFn {
pub fn eval(&self, t: f64) -> f64 {
match &self.0 {
Inner::Custom(e) => e.eval(&[t]),
Inner::Builtin(s) => match *s {
ExcitationSignal::Gaussian { f0, .. } => {
let (Some(tau), Some(t0)) = (s.gaussian_width(), s.gaussian_delay()) else {
return 0.0;
};
let x = (t - t0) / tau;
(2.0 * PI * f0 * (t - t0)).cos() * (-x * x).exp()
}
ExcitationSignal::Sinus { f0 } => (2.0 * PI * f0 * t).sin(),
ExcitationSignal::Dirac { .. } => {
if t == 0.0 {
1.0
} else {
0.0
}
}
ExcitationSignal::Step { .. } => {
if t >= 0.0 {
1.0
} else {
0.0
}
}
ExcitationSignal::Custom { .. } => unreachable!("compiled separately"),
},
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Boundary {
#[default]
Pec,
Pmc,
Mur,
Pml(u32),
}
impl Boundary {
fn parse_opt(s: &str) -> Option<Boundary> {
let t = s.trim().to_ascii_uppercase();
match t.as_str() {
"PEC" | "0" => return Some(Boundary::Pec),
"PMC" | "1" => return Some(Boundary::Pmc),
"MUR" | "2" => return Some(Boundary::Mur),
"PML" | "3" => return Some(Boundary::Pml(8)),
_ => {}
}
let rest = t.strip_prefix("PML")?.trim_start_matches(['_', ' ', '-']);
rest.parse().ok().map(Boundary::Pml)
}
}
impl core::str::FromStr for Boundary {
type Err = Error;
fn from_str(s: &str) -> Result<Boundary> {
Boundary::parse_opt(s)
.ok_or_else(|| Error::Format(format!("unknown boundary condition {s:?}")))
}
}
impl core::fmt::Display for Boundary {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Boundary::Pec => f.write_str("PEC"),
Boundary::Pmc => f.write_str("PMC"),
Boundary::Mur => f.write_str("MUR"),
Boundary::Pml(n) => write!(f, "PML_{n}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct BoundaryConditions {
pub faces: [Boundary; 6],
pub mur_phase_velocity: Option<f64>,
pub pml_grading: Option<String>,
pub extra_attributes: Vec<(String, String)>,
}
pub const FACE_NAMES: [&str; 6] = ["xmin", "xmax", "ymin", "ymax", "zmin", "zmax"];
impl BoundaryConditions {
pub fn all(b: Boundary) -> Self {
BoundaryConditions {
faces: [b; 6],
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FdtdSettings {
pub number_of_timesteps: Option<u64>,
pub end_criteria: Option<f64>,
pub f_max: Option<f64>,
pub over_sampling: Option<u32>,
pub time_step_method: Option<TimeStepMethod>,
pub max_time: Option<f64>,
pub time_step: Option<f64>,
pub time_step_factor: Option<f64>,
pub cell_constant_material: Option<bool>,
pub coord_system: Option<i32>,
pub multi_grid: Option<String>,
pub excitation: Option<ExcitationSignal>,
pub excitation_extra_attributes: Vec<(String, String)>,
pub boundary: Option<BoundaryConditions>,
pub extra_attributes: Vec<(String, String)>,
}
impl FdtdSettings {
pub fn new() -> Self {
Self::default()
}
pub fn gaussian(f0: f64, fc: f64) -> Self {
FdtdSettings {
number_of_timesteps: Some(1_000_000_000),
end_criteria: Some(1e-5),
f_max: Some(f0 + fc),
excitation: Some(ExcitationSignal::Gaussian { f0, fc }),
boundary: Some(BoundaryConditions::all(Boundary::Pml(8))),
..Default::default()
}
}
pub fn validate(&self) -> Result<()> {
let bad = |m: &str| Err(Error::Invalid(m.into()));
if self.end_criteria.is_some_and(|e| e.is_nan() || e <= 0.0) {
return bad("end criteria must be positive");
}
if let Some(ExcitationSignal::Gaussian { f0, fc }) = self.excitation
&& (fc.is_nan() || fc <= 0.0 || f0 < 0.0)
{
return bad("Gaussian excitation needs fc > 0 and f0 >= 0");
}
if let Some(e @ ExcitationSignal::Custom { .. }) = &self.excitation {
e.evaluator()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeStepMethod {
Cfl,
Rennings,
Other(i32),
}
impl TimeStepMethod {
pub const fn from_id(id: i32) -> Self {
match id {
1 => TimeStepMethod::Cfl,
3 => TimeStepMethod::Rennings,
o => TimeStepMethod::Other(o),
}
}
pub const fn id(self) -> i32 {
match self {
TimeStepMethod::Cfl => 1,
TimeStepMethod::Rennings => 3,
TimeStepMethod::Other(o) => o,
}
}
}
impl From<i32> for TimeStepMethod {
fn from(id: i32) -> Self {
TimeStepMethod::from_id(id)
}
}
impl From<TimeStepMethod> for i32 {
fn from(m: TimeStepMethod) -> i32 {
m.id()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn boundaries() {
assert_eq!("PML_8".parse::<Boundary>().ok(), Some(Boundary::Pml(8)));
assert_eq!("PML 12".parse::<Boundary>().ok(), Some(Boundary::Pml(12)));
assert_eq!("pml".parse::<Boundary>().ok(), Some(Boundary::Pml(8)));
assert_eq!("1".parse::<Boundary>().ok(), Some(Boundary::Pmc));
assert_eq!("3".parse::<Boundary>().ok(), Some(Boundary::Pml(8)));
assert_eq!(" MUR ".parse::<Boundary>().ok(), Some(Boundary::Mur));
assert_eq!("PEC".parse::<Boundary>().ok(), Some(Boundary::Pec));
assert_eq!("PML_x".parse::<Boundary>().ok(), None);
assert_eq!("foo".parse::<Boundary>().ok(), None);
assert_eq!(Boundary::Pml(16).to_string(), "PML_16");
}
#[test]
fn gaussian() {
let g = ExcitationSignal::Gaussian { f0: 1e9, fc: 5e8 };
let tau = g.gaussian_width().unwrap();
let t0 = g.gaussian_delay().unwrap();
assert!((tau - 10f64.ln().sqrt() / (PI * 5e8)).abs() < 1e-20);
assert!((g.eval(t0) - 1.0).abs() < 1e-12);
assert!(g.eval(0.0).abs() <= 1.0001e-6);
assert!(g.eval(g.duration().unwrap()).abs() <= 1.0001e-6);
assert_eq!(g.max_frequency(), 1.5e9);
assert_eq!(g.id(), 0);
}
#[test]
fn other_signals() {
let s = ExcitationSignal::Sinus { f0: 1.0 };
assert!((s.eval(0.25) - 1.0).abs() < 1e-12);
assert_eq!(ExcitationSignal::Step { f_max: 1e9 }.eval(-1.0), 0.0);
assert_eq!(ExcitationSignal::Step { f_max: 1e9 }.eval(1.0), 1.0);
assert_eq!(ExcitationSignal::Dirac { f_max: 1e9 }.eval(0.0), 1.0);
let c = ExcitationSignal::Custom {
function: "sin(2*pi*f0*t)".into(),
f0: 2.0,
f_max: 4.0,
};
assert!((c.eval(0.125) - 1.0).abs() < 1e-12);
let bad = ExcitationSignal::Custom {
function: "q*t".into(),
f0: 1.0,
f_max: 1.0,
};
assert!(bad.eval(1.0).is_nan());
assert!(bad.evaluator().is_err());
}
#[test]
fn validation() {
assert!(FdtdSettings::gaussian(1e9, 5e8).validate().is_ok());
let mut s = FdtdSettings::new();
s.excitation = Some(ExcitationSignal::Gaussian { f0: 1e9, fc: 0.0 });
assert!(s.validate().is_err());
}
}