use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct LumericalDomain {
pub x_span: f64,
pub y_span: f64,
pub z_span: f64,
pub dx: f64,
pub time_steps: usize,
pub wavelength_start: f64,
pub wavelength_stop: f64,
}
impl Default for LumericalDomain {
fn default() -> Self {
Self {
x_span: 10e-6,
y_span: 10e-6,
z_span: 10e-6,
dx: 10e-9,
time_steps: 1000,
wavelength_start: 1500e-9,
wavelength_stop: 1600e-9,
}
}
}
#[derive(Debug, Clone)]
pub struct LumericalGeometry {
pub kind: String,
pub material: String,
pub properties: HashMap<String, f64>,
}
#[derive(Debug, Clone)]
pub struct LumericalSource {
pub kind: String,
pub properties: HashMap<String, f64>,
pub str_properties: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct LumericalMonitor {
pub kind: String,
pub properties: HashMap<String, f64>,
pub str_properties: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct LumericalSimulation {
pub domain: LumericalDomain,
pub geometries: Vec<LumericalGeometry>,
pub sources: Vec<LumericalSource>,
pub monitors: Vec<LumericalMonitor>,
}
pub struct LumericalParser;
impl LumericalParser {
pub fn parse(text: &str) -> LumericalSimulation {
let mut domain = LumericalDomain::default();
let mut geometries = Vec::new();
let mut sources = Vec::new();
let mut monitors = Vec::new();
let mut current_geo: Option<LumericalGeometry> = None;
let mut current_source: Option<LumericalSource> = None;
let mut current_monitor: Option<LumericalMonitor> = None;
for raw_line in text.lines() {
let line = raw_line.trim();
if line.starts_with('#') || line.starts_with('%') || line.is_empty() {
continue;
}
if line.starts_with("addrect") {
Self::flush_geo(&mut current_geo, &mut geometries);
Self::flush_source(&mut current_source, &mut sources);
Self::flush_monitor(&mut current_monitor, &mut monitors);
current_geo = Some(LumericalGeometry {
kind: "rectangle".into(),
material: "etch".into(),
properties: HashMap::new(),
});
} else if line.starts_with("addcircle") {
Self::flush_geo(&mut current_geo, &mut geometries);
current_geo = Some(LumericalGeometry {
kind: "circle".into(),
material: "".into(),
properties: HashMap::new(),
});
} else if line.starts_with("addmodesource") || line.starts_with("addmode") {
Self::flush_geo(&mut current_geo, &mut geometries);
Self::flush_source(&mut current_source, &mut sources);
current_source = Some(LumericalSource {
kind: "mode".into(),
properties: HashMap::new(),
str_properties: HashMap::new(),
});
} else if line.starts_with("addplane") {
Self::flush_source(&mut current_source, &mut sources);
current_source = Some(LumericalSource {
kind: "plane".into(),
properties: HashMap::new(),
str_properties: HashMap::new(),
});
} else if line.starts_with("addpower") || line.starts_with("addprofile") {
Self::flush_monitor(&mut current_monitor, &mut monitors);
current_monitor = Some(LumericalMonitor {
kind: "DFT".into(),
properties: HashMap::new(),
str_properties: HashMap::new(),
});
}
if line.contains("setnamed(") {
if let Some((_obj, key, val)) = Self::parse_setnamed(line) {
if let Ok(v) = val.parse::<f64>() {
Self::apply_domain_setting(&key, v, &mut domain);
}
}
}
if line.contains("set(") && !line.contains("setnamed(") {
if let Some((key, val)) = Self::parse_set(line) {
if let Ok(v) = val.parse::<f64>() {
Self::apply_numeric_setting(
&key,
v,
&mut domain,
&mut current_geo,
&mut current_source,
&mut current_monitor,
);
} else {
let s = val.trim_matches('"').to_string();
Self::apply_string_setting(
&key,
s,
&mut current_geo,
&mut current_source,
&mut current_monitor,
);
}
}
}
}
Self::flush_geo(&mut current_geo, &mut geometries);
Self::flush_source(&mut current_source, &mut sources);
Self::flush_monitor(&mut current_monitor, &mut monitors);
LumericalSimulation {
domain,
geometries,
sources,
monitors,
}
}
pub fn parse_file(path: &str) -> Result<LumericalSimulation, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("cannot read {path}: {e}"))?;
Ok(Self::parse(&text))
}
pub fn summarize(sim: &LumericalSimulation) -> String {
let mut out = String::new();
out.push_str(&format!(
"Domain: {:.1}×{:.1}×{:.1} µm\n",
sim.domain.x_span * 1e6,
sim.domain.y_span * 1e6,
sim.domain.z_span * 1e6,
));
out.push_str(&format!("Mesh: dx={:.1} nm\n", sim.domain.dx * 1e9));
out.push_str(&format!(
"Wavelength: {:.0}–{:.0} nm\n",
sim.domain.wavelength_start * 1e9,
sim.domain.wavelength_stop * 1e9,
));
out.push_str(&format!("Geometries: {}\n", sim.geometries.len()));
out.push_str(&format!("Sources: {}\n", sim.sources.len()));
out.push_str(&format!("Monitors: {}\n", sim.monitors.len()));
out
}
fn parse_set(line: &str) -> Option<(String, String)> {
let start = line.find('"')?;
let rest = &line[start + 1..];
let end = rest.find('"')?;
let key = rest[..end].to_string();
let after = &rest[end + 1..];
let comma = after.find(',')?;
let val_str = after[comma + 1..]
.trim()
.trim_end_matches(");")
.trim()
.to_string();
Some((key, val_str))
}
fn apply_numeric_setting(
key: &str,
val: f64,
domain: &mut LumericalDomain,
geo: &mut Option<LumericalGeometry>,
source: &mut Option<LumericalSource>,
monitor: &mut Option<LumericalMonitor>,
) {
if let Some(g) = geo {
g.properties.insert(key.to_string(), val);
return;
}
if let Some(s) = source {
s.properties.insert(key.to_string(), val);
return;
}
if let Some(m) = monitor {
m.properties.insert(key.to_string(), val);
return;
}
Self::apply_domain_setting(key, val, domain);
}
fn apply_domain_setting(key: &str, val: f64, domain: &mut LumericalDomain) {
match key {
"x span" | "x_span" => domain.x_span = val,
"y span" | "y_span" => domain.y_span = val,
"z span" | "z_span" => domain.z_span = val,
"dx" | "mesh cells x" => domain.dx = val,
"simulation time" => domain.time_steps = (val / 1e-15) as usize,
"wavelength start" => domain.wavelength_start = val,
"wavelength stop" => domain.wavelength_stop = val,
_ => {}
}
}
fn parse_setnamed(line: &str) -> Option<(String, String, String)> {
let s1 = line.find('"')? + 1;
let e1 = line[s1..].find('"')? + s1;
let obj = line[s1..e1].to_string();
let s2 = line[e1 + 1..].find('"')? + e1 + 2;
let e2 = line[s2..].find('"')? + s2;
let key = line[s2..e2].to_string();
let after = &line[e2 + 1..];
let comma = after.find(',')?;
let val = after[comma + 1..]
.trim()
.trim_end_matches(");")
.trim()
.to_string();
Some((obj, key, val))
}
fn apply_string_setting(
key: &str,
val: String,
geo: &mut Option<LumericalGeometry>,
source: &mut Option<LumericalSource>,
monitor: &mut Option<LumericalMonitor>,
) {
if key == "material" {
if let Some(g) = geo {
g.material = val;
return;
}
}
if let Some(g) = geo {
g.str_properties_insert(key, val);
} else if let Some(s) = source {
s.str_properties.insert(key.to_string(), val);
} else if let Some(m) = monitor {
m.str_properties.insert(key.to_string(), val);
}
}
fn flush_geo(geo: &mut Option<LumericalGeometry>, list: &mut Vec<LumericalGeometry>) {
if let Some(g) = geo.take() {
list.push(g);
}
}
fn flush_source(src: &mut Option<LumericalSource>, list: &mut Vec<LumericalSource>) {
if let Some(s) = src.take() {
list.push(s);
}
}
fn flush_monitor(mon: &mut Option<LumericalMonitor>, list: &mut Vec<LumericalMonitor>) {
if let Some(m) = mon.take() {
list.push(m);
}
}
}
trait StrPropsExt {
fn str_properties_insert(&mut self, key: &str, val: String);
}
impl StrPropsExt for LumericalGeometry {
fn str_properties_insert(&mut self, _key: &str, _val: String) {
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_SCRIPT: &str = r#"
# Lumerical FDTD script
setnamed("FDTD", "x span", 20e-6);
setnamed("FDTD", "y span", 10e-6);
setnamed("FDTD", "z span", 5e-6);
setnamed("FDTD", "wavelength start", 1500e-9);
setnamed("FDTD", "wavelength stop", 1600e-9);
addrect;
set("x span", 5e-6);
set("y span", 220e-9);
set("material", "Si (Silicon) - Palik");
addmodesource;
set("wavelength start", 1500e-9);
set("wavelength stop", 1600e-9);
addpower;
set("x span", 1e-6);
"#;
#[test]
fn parse_domain_x_span() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert!(
(sim.domain.x_span - 20e-6).abs() < 1e-15,
"x_span={}",
sim.domain.x_span
);
}
#[test]
fn parse_domain_wavelength() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert!((sim.domain.wavelength_start - 1500e-9).abs() < 1e-18);
assert!((sim.domain.wavelength_stop - 1600e-9).abs() < 1e-18);
}
#[test]
fn parse_geometry_found() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert_eq!(sim.geometries.len(), 1);
assert_eq!(sim.geometries[0].kind, "rectangle");
}
#[test]
fn parse_source_found() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert_eq!(sim.sources.len(), 1);
assert_eq!(sim.sources[0].kind, "mode");
}
#[test]
fn parse_monitor_found() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert_eq!(sim.monitors.len(), 1);
}
#[test]
fn geometry_material_parsed() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
assert!(
sim.geometries[0].material.contains("Si"),
"material={}",
sim.geometries[0].material
);
}
#[test]
fn geometry_x_span_parsed() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
let xspan = sim.geometries[0]
.properties
.get("x span")
.copied()
.unwrap_or(0.0);
assert!((xspan - 5e-6).abs() < 1e-15, "xspan={xspan}");
}
#[test]
fn summarize_contains_domain() {
let sim = LumericalParser::parse(SAMPLE_SCRIPT);
let s = LumericalParser::summarize(&sim);
assert!(s.contains("Domain:"), "summary={s}");
assert!(s.contains("Wavelength:"), "summary={s}");
}
#[test]
fn empty_script_gives_defaults() {
let sim = LumericalParser::parse("");
assert_eq!(sim.geometries.len(), 0);
assert_eq!(sim.sources.len(), 0);
}
#[test]
fn parse_set_extracts_key_value() {
let result = LumericalParser::parse_set(r#"set("x span", 5e-6);"#);
assert!(result.is_some());
let (key, val) = result.unwrap();
assert_eq!(key, "x span");
assert!((val.parse::<f64>().unwrap() - 5e-6).abs() < 1e-20);
}
}