use num_complex::Complex64 as c64;
use super::Polarization;
use crate::units::Wavelength;
use crate::{Error, Result};
#[derive(Clone, Debug, PartialEq)]
pub struct Profile {
nodes: Vec<f64>,
cells: Vec<c64>,
pml: (f64, f64),
strength: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProfileMode {
pub effective_index: c64,
pub field: Vec<c64>,
}
impl Profile {
pub fn new(nodes: Vec<f64>, cells: Vec<c64>) -> Result<Profile> {
if nodes.len() < 3
|| nodes.iter().any(|x| !x.is_finite())
|| nodes.windows(2).any(|w| w[1] <= w[0])
{
return Err(Error::invalid(
"profile",
"needs at least 3 finite, strictly increasing nodes",
));
}
if cells.len() != nodes.len() - 1
|| cells
.iter()
.any(|e| !(e.re.is_finite() && e.im.is_finite()))
{
return Err(Error::invalid(
"profile",
format!(
"needs {} finite cells, got {}",
nodes.len() - 1,
cells.len()
),
));
}
Ok(Profile {
nodes,
cells,
pml: (0.0, 0.0),
strength: 0.0,
})
}
pub fn uniform(x0: f64, x1: f64, n: usize, eps: impl Fn(f64) -> c64) -> Result<Profile> {
let nodes: Vec<f64> = (0..=n)
.map(|i| x0 + (x1 - x0) * i as f64 / n as f64)
.collect();
let cells = nodes.windows(2).map(|w| eps(0.5 * (w[0] + w[1]))).collect();
Profile::new(nodes, cells)
}
pub fn with_pml(mut self, low: f64, high: f64, strength: f64) -> Result<Profile> {
let ok = |v: f64| v.is_finite() && v >= 0.0;
if !(ok(low) && ok(high) && ok(strength)) {
return Err(Error::invalid(
"PML",
"thicknesses and strength must be finite and not negative",
));
}
if low + high >= self.nodes[self.nodes.len() - 1] - self.nodes[0] {
return Err(Error::invalid(
"PML",
"the layers are thicker than the profile",
));
}
self.pml = (low, high);
self.strength = strength;
Ok(self)
}
pub fn nodes(&self) -> &[f64] {
&self.nodes
}
pub fn modes(
&self,
polarization: Polarization,
wavelength: Wavelength,
count: usize,
near: Option<f64>,
) -> Result<Vec<ProfileMode>> {
if count == 0 {
return Err(Error::invalid("mode count", "must be at least 1"));
}
let k = wavelength.wavenumber();
let k2 = k * k;
let (start, end) = (self.nodes[0], self.nodes[self.nodes.len() - 1]);
let x: Vec<c64> = self
.nodes
.iter()
.map(|&p| {
let mut im = 0.0;
if self.pml.0 > 0.0 && p < start + self.pml.0 {
im -= self.strength * (start + self.pml.0 - p).powi(3)
/ (3.0 * self.pml.0 * self.pml.0);
}
if self.pml.1 > 0.0 && p > end - self.pml.1 {
im += self.strength * (p - (end - self.pml.1)).powi(3)
/ (3.0 * self.pml.1 * self.pml.1);
}
c64::new(p, im)
})
.collect();
let n = x.len();
let last = self.cells.len() - 1;
let mut entries = Vec::with_capacity(3 * n);
for i in 0..n {
let w = if i > 0 { x[i] - x[i - 1] } else { x[1] - x[0] };
let e = if i < n - 1 {
x[i + 1] - x[i]
} else {
x[n - 1] - x[n - 2]
};
let (ew, ee) = (
self.cells[i.saturating_sub(1).min(last)],
self.cells[i.min(last)],
);
let s = 2.0 / (w + e);
match polarization {
Polarization::Te => {
let (cw, ce) = (s / w, s / e);
let diag = -(cw + ce) + k2 * (w * ew + e * ee) / (w + e);
entries.push((i, i, diag));
if i > 0 {
entries.push((i, i - 1, cw));
}
if i < n - 1 {
entries.push((i, i + 1, ce));
}
}
Polarization::Tm => {
let inv = (w / ew + e / ee) / (w + e);
let (cw, ce) = (s / (w * ew) / inv, s / (e * ee) / inv);
let diag = -(cw + ce) + k2 / inv;
entries.push((i, i, diag));
if i > 0 {
entries.push((i, i - 1, cw));
}
if i < n - 1 {
entries.push((i, i + 1, ce));
}
}
}
}
let n_max =
near.unwrap_or_else(|| self.cells.iter().map(|e| e.re.sqrt()).fold(1.0, f64::max));
let shift = c64::new(k2 * n_max * n_max, 0.0);
let pairs = crate::eigen::nearest(n, &entries, shift, count, 1e-10)?;
Ok(pairs
.into_iter()
.map(|p| {
let peak = p
.vector
.iter()
.copied()
.max_by(|a, b| a.norm().total_cmp(&b.norm()))
.unwrap_or(c64::new(1.0, 0.0));
ProfileMode {
effective_index: p.value.sqrt() / k,
field: p.vector.iter().map(|v| v / peak).collect(),
}
})
.collect())
}
}
pub(crate) fn chilwell_profile(h: f64) -> Profile {
let index = |x: f64| match x {
x if x > 0.0 => 1.0,
x if x > -0.5 => 1.66,
x if x > -1.0 => 1.53,
x if x > -1.5 => 1.60,
x if x > -2.0 => 1.66,
_ => 1.5,
};
Profile::uniform(-8.0, 1.0, (9.0 / h).round() as usize, |x| {
c64::new(index(x) * index(x), 0.0)
})
.expect("a valid profile")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mode::multilayer::chilwell_four_layer;
use crate::mode::slab::Slab;
use crate::units::Length;
fn lam() -> Wavelength {
Wavelength::um(1.55).unwrap()
}
fn book(h: f64) -> Profile {
Profile::uniform(-2.01, 2.01, (4.02 / h).round() as usize, |x| {
let n: f64 = if x.abs() < 0.11 { 3.473 } else { 1.444 };
c64::new(n * n, 0.0)
})
.unwrap()
}
#[test]
fn the_slab_converges_at_second_order_to_the_exact_one() {
let slab = Slab::new(1.444, 3.473, 1.444, Length::nm(220.0)).unwrap();
for pol in [Polarization::Te, Polarization::Tm] {
let exact = slab.modes(pol, lam())[0].effective_index();
let errors: Vec<f64> = [0.01, 0.005, 0.0025]
.iter()
.map(|&h| {
book(h).modes(pol, lam(), 1, None).unwrap()[0]
.effective_index
.re
- exact
})
.collect();
for w in errors.windows(2) {
let order = (w[0] / w[1]).abs().log2();
assert!((order - 2.0).abs() < 0.1, "{pol:?}: {errors:?}");
}
}
}
#[test]
fn the_four_layer_guide_has_chilwell_and_hodgkinsons_modes() {
let (stack, w) = chilwell_four_layer();
let profile = chilwell_profile(0.001);
for pol in [Polarization::Te, Polarization::Tm] {
let exact = stack.bound_modes(pol, w).unwrap();
let found = profile.modes(pol, w, 4, Some(1.63)).unwrap();
let mut got: Vec<f64> = found.iter().map(|m| m.effective_index.re).collect();
got.sort_by(|a, b| b.total_cmp(a));
for (g, e) in got.iter().zip(&exact) {
assert!(
(g - e.effective_index().re).abs() < 2e-6,
"{pol:?}: {g} vs {}",
e.effective_index().re
);
}
}
}
#[test]
fn a_pml_gives_the_leaky_waves() {
let profile = chilwell_profile(0.001).with_pml(2.0, 0.0, 5.0).unwrap();
let w = Wavelength::nm(632.8).unwrap();
for (re, im) in [
(1.46186, 0.00716),
(1.38250, 0.01817),
(1.28136, 0.03588),
(1.14231, 0.05288),
] {
let target = c64::new(re, im);
let best = profile
.modes(Polarization::Te, w, 3, Some(re))
.unwrap()
.into_iter()
.map(|m| m.effective_index)
.min_by(|a, b| (a - target).norm().total_cmp(&(b - target).norm()))
.unwrap();
assert!((best - target).norm() < 2e-5, "{best} vs {target}");
}
}
#[test]
fn bad_profiles_are_errors() {
assert!(Profile::new(vec![0.0, 1.0], vec![c64::new(1.0, 0.0)]).is_err());
assert!(Profile::new(vec![0.0, 1.0, 1.0], vec![c64::new(1.0, 0.0); 2]).is_err());
assert!(Profile::new(vec![0.0, 1.0, 2.0], vec![c64::new(1.0, 0.0)]).is_err());
assert!(book(0.01).with_pml(3.0, 2.0, 3.0).is_err());
}
}