use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct WaveguideBend {
pub radius_um: f64,
pub angle_deg: f64,
pub width_nm: f64,
pub loss_db: f64,
}
impl WaveguideBend {
pub fn new(radius_um: f64, angle_deg: f64, width_nm: f64) -> Self {
Self {
radius_um,
angle_deg,
width_nm,
loss_db: 0.0,
}
}
pub fn bend_loss_db(&self, n_eff: f64, n_clad: f64, wavelength: f64) -> f64 {
let delta_n = (n_eff - n_clad).max(1.0e-6);
let k0 = 2.0 * PI / wavelength;
let gamma = k0 * (n_eff * n_eff - n_clad * n_clad).sqrt().max(1.0e-10);
let r_m = self.radius_um * 1.0e-6;
let exponent = -2.0 / 3.0 * gamma * r_m * (delta_n / n_eff).powf(1.5);
let alpha_per_rad = (PI * k0 * k0) / (gamma * r_m) * exponent.exp();
let arc_length_m = r_m * self.angle_deg * PI / 180.0;
let loss_nepers = alpha_per_rad * self.angle_deg * PI / 180.0 * arc_length_m;
loss_nepers * 10.0 / (10.0_f64).ln()
}
pub fn euler_transition_length_um(&self) -> f64 {
0.4 * self.radius_um
}
pub fn min_radius_for_loss(
&self,
target_db: f64,
n_eff: f64,
n_clad: f64,
wavelength: f64,
) -> f64 {
let mut lo = 1.0_f64;
let mut hi = 1000.0_f64;
let test_angle = self.angle_deg;
for _ in 0..60 {
let mid = (lo + hi) / 2.0;
let b = WaveguideBend::new(mid, test_angle, self.width_nm);
let loss = b.bend_loss_db(n_eff, n_clad, wavelength);
if loss > target_db {
lo = mid;
} else {
hi = mid;
}
}
hi
}
}
#[derive(Debug, Clone)]
pub struct SBend {
pub length_um: f64,
pub offset_um: f64,
pub width_nm: f64,
}
impl SBend {
pub fn new(length_um: f64, offset_um: f64, width_nm: f64) -> Self {
Self {
length_um,
offset_um,
width_nm,
}
}
pub fn minimum_radius_um(&self) -> f64 {
let l = self.length_um;
let d = self.offset_um.abs().max(1.0e-6);
l * l / (2.0 * PI * PI * d)
}
pub fn insertion_loss_db(&self, n_eff: f64, n_clad: f64, wavelength: f64) -> f64 {
let r_min = self.minimum_radius_um().max(0.1);
let bend = WaveguideBend::new(r_min, 90.0, self.width_nm);
2.0 * bend.bend_loss_db(n_eff, n_clad, wavelength)
}
pub fn adiabatic_length_um(&self, n_eff: f64, wavelength: f64) -> f64 {
let lambda_um = wavelength * 1.0e6;
let delta_n = 0.01 * n_eff; let l_beat_um = lambda_um / (2.0 * delta_n);
5.0 * l_beat_um * self.offset_um.abs() / lambda_um
}
}
#[derive(Debug, Clone)]
pub struct WaveguideCrossing {
pub width_nm: f64,
pub insertion_loss_db: f64,
pub crosstalk_db: f64,
pub back_reflection_db: f64,
}
impl WaveguideCrossing {
pub fn new_broadened(width_nm: f64) -> Self {
let broadened = width_nm * 3.0;
let il = 0.05 + 1000.0 / broadened;
let xt = -20.0 - 10.0 * (broadened / 1000.0).log10();
Self {
width_nm,
insertion_loss_db: il.clamp(0.01, 1.0),
crosstalk_db: xt.clamp(-60.0, -10.0),
back_reflection_db: -40.0,
}
}
pub fn new_mmi_based(width_nm: f64) -> Self {
Self {
width_nm,
insertion_loss_db: 0.1,
crosstalk_db: -40.0,
back_reflection_db: -45.0,
}
}
pub fn total_loss_db(&self) -> f64 {
let crosstalk_linear = 10.0_f64.powf(self.crosstalk_db / 10.0);
let il_linear = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
-10.0 * (il_linear * (1.0 - crosstalk_linear)).log10()
}
}
#[derive(Debug, Clone)]
pub enum TaperShape {
Linear,
Parabolic,
Exponential,
Adiabatic {
mode_overlap_db: f64,
},
}
#[derive(Debug, Clone)]
pub struct WaveguideTaper {
pub length_um: f64,
pub width_start_nm: f64,
pub width_end_nm: f64,
pub taper_type: TaperShape,
}
impl WaveguideTaper {
pub fn new_linear(length: f64, w_start: f64, w_end: f64) -> Self {
Self {
length_um: length,
width_start_nm: w_start,
width_end_nm: w_end,
taper_type: TaperShape::Linear,
}
}
pub fn new_adiabatic(
w_start: f64,
w_end: f64,
target_loss_db: f64,
n_eff: f64,
wavelength: f64,
) -> Self {
let lambda_nm = wavelength * 1.0e9;
let dw = (w_end - w_start).abs();
let w_avg = (w_start + w_end) / 2.0;
let safety = 1.0 + 1.0 / target_loss_db.max(0.01);
let length_um = safety * dw * n_eff / (lambda_nm / w_avg) / 1000.0;
Self {
length_um: length_um.max(5.0),
width_start_nm: w_start,
width_end_nm: w_end,
taper_type: TaperShape::Adiabatic {
mode_overlap_db: target_loss_db,
},
}
}
pub fn width_at(&self, z_um: f64) -> f64 {
let t = (z_um / self.length_um).clamp(0.0, 1.0);
let w0 = self.width_start_nm;
let w1 = self.width_end_nm;
match &self.taper_type {
TaperShape::Linear => w0 + (w1 - w0) * t,
TaperShape::Parabolic => w0 + (w1 - w0) * t * t,
TaperShape::Exponential => {
if (w1 / w0.max(1.0e-9)).abs() < 1.0e-12 {
w0
} else {
w0 * (w1 / w0.max(1.0e-9)).ln().exp() * t.exp()
}
}
TaperShape::Adiabatic { .. } => w0 + (w1 - w0) * t,
}
}
pub fn insertion_loss_db(&self, n_eff_start: f64, n_eff_end: f64) -> f64 {
if (n_eff_start - n_eff_end).abs() < 1.0e-9 {
return 0.0;
}
let r1 = 1.0 / n_eff_start.sqrt();
let r2 = 1.0 / n_eff_end.sqrt();
let overlap = 4.0 * r1 * r2 / ((r1 + r2) * (r1 + r2));
-10.0 * overlap.log10()
}
pub fn is_adiabatic(&self, n_eff: f64, n_clad: f64, wavelength: f64) -> bool {
let dw_dz = (self.width_end_nm - self.width_start_nm).abs() / self.length_um;
let w_avg = (self.width_start_nm + self.width_end_nm) / 2.0;
let lambda_nm = wavelength * 1.0e9;
let threshold = lambda_nm * n_eff / (n_clad * w_avg);
dw_dz < threshold
}
}
#[derive(Debug, Clone)]
pub enum RouteSegment {
Straight {
length_um: f64,
angle_deg: f64,
},
Bend {
radius_um: f64,
angle_deg: f64,
},
Taper {
length_um: f64,
w_start_nm: f64,
w_end_nm: f64,
},
}
impl RouteSegment {
pub fn length_um(&self) -> f64 {
match self {
Self::Straight { length_um, .. } => *length_um,
Self::Bend {
radius_um,
angle_deg,
} => radius_um * angle_deg.abs() * PI / 180.0,
Self::Taper { length_um, .. } => *length_um,
}
}
}
#[derive(Debug, Clone)]
pub struct WaveguideRoute {
pub segments: Vec<RouteSegment>,
pub total_length_um: f64,
pub n_bends: usize,
}
impl WaveguideRoute {
fn from_segments(segments: Vec<RouteSegment>) -> Self {
let total_length_um = segments.iter().map(|s| s.length_um()).sum();
let n_bends = segments
.iter()
.filter(|s| matches!(s, RouteSegment::Bend { .. }))
.count();
Self {
segments,
total_length_um,
n_bends,
}
}
}
#[derive(Debug, Clone)]
pub struct PicRouter {
pub grid_size_um: f64,
pub min_spacing_um: f64,
pub bend_radius_um: f64,
pub waveguide_width_nm: f64,
}
impl PicRouter {
pub fn new(grid_um: f64, min_spacing: f64, bend_radius: f64, width_nm: f64) -> Self {
Self {
grid_size_um: grid_um,
min_spacing_um: min_spacing,
bend_radius_um: bend_radius,
waveguide_width_nm: width_nm,
}
}
fn snap(&self, v: f64) -> f64 {
(v / self.grid_size_um).round() * self.grid_size_um
}
pub fn route(
&self,
port_a: [f64; 2],
dir_a: f64,
port_b: [f64; 2],
_dir_b: f64,
) -> WaveguideRoute {
let r = self.bend_radius_um;
let dx = self.snap(port_b[0] - port_a[0]);
let dy = self.snap(port_b[1] - port_a[1]);
let dir_a_rad = dir_a * PI / 180.0;
let is_horizontal = dir_a_rad.cos().abs() > dir_a_rad.sin().abs();
let mut segs: Vec<RouteSegment> = Vec::new();
if is_horizontal {
let horiz = dx.abs() - r;
let vert = dy.abs() - r;
if horiz > 0.0 {
segs.push(RouteSegment::Straight {
length_um: horiz,
angle_deg: dir_a,
});
}
let turn = if dy >= 0.0 { 90.0 } else { -90.0 };
segs.push(RouteSegment::Bend {
radius_um: r,
angle_deg: turn,
});
if vert > 0.0 {
segs.push(RouteSegment::Straight {
length_um: vert,
angle_deg: dir_a + turn,
});
}
segs.push(RouteSegment::Bend {
radius_um: r,
angle_deg: -turn,
});
} else {
let vert = dy.abs() - r;
let horiz = dx.abs() - r;
if vert > 0.0 {
segs.push(RouteSegment::Straight {
length_um: vert,
angle_deg: dir_a,
});
}
let turn = if dx >= 0.0 { -90.0 } else { 90.0 };
segs.push(RouteSegment::Bend {
radius_um: r,
angle_deg: turn,
});
if horiz > 0.0 {
segs.push(RouteSegment::Straight {
length_um: horiz,
angle_deg: dir_a + turn,
});
}
segs.push(RouteSegment::Bend {
radius_um: r,
angle_deg: -turn,
});
}
WaveguideRoute::from_segments(segs)
}
pub fn route_loss_db(
&self,
route: &WaveguideRoute,
loss_db_per_cm: f64,
bend_loss_db: f64,
) -> f64 {
let straight_cm = route.total_length_um * 1.0e-4; let propagation_loss = straight_cm * loss_db_per_cm;
let bend_loss = route.n_bends as f64 * bend_loss_db;
propagation_loss + bend_loss
}
pub fn route_length_um(&self, route: &WaveguideRoute) -> f64 {
route.total_length_um
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_bend_euler_transition_proportional() {
let b = WaveguideBend::new(10.0, 90.0, 450.0);
assert_abs_diff_eq!(b.euler_transition_length_um(), 4.0, epsilon = 1.0e-10);
}
#[test]
fn test_bend_loss_increases_for_smaller_radius() {
let n_eff = 2.45;
let n_clad = 1.44;
let wl = 1.55e-6;
let b_small = WaveguideBend::new(2.0, 90.0, 450.0);
let b_large = WaveguideBend::new(20.0, 90.0, 450.0);
let loss_small = b_small.bend_loss_db(n_eff, n_clad, wl);
let loss_large = b_large.bend_loss_db(n_eff, n_clad, wl);
assert!(
loss_small >= loss_large,
"Smaller radius should have >= loss"
);
}
#[test]
fn test_sbend_minimum_radius_formula() {
let sb = SBend::new(100.0, 10.0, 450.0);
let r = sb.minimum_radius_um();
assert_abs_diff_eq!(r, 100.0_f64.powi(2) / (2.0 * PI * PI * 10.0), epsilon = 0.1);
}
#[test]
fn test_taper_width_at_endpoints() {
let t = WaveguideTaper::new_linear(50.0, 300.0, 900.0);
assert_abs_diff_eq!(t.width_at(0.0), 300.0, epsilon = 1.0e-6);
assert_abs_diff_eq!(t.width_at(50.0), 900.0, epsilon = 1.0e-6);
}
#[test]
fn test_taper_midpoint_linear() {
let t = WaveguideTaper::new_linear(100.0, 400.0, 1000.0);
assert_abs_diff_eq!(t.width_at(50.0), 700.0, epsilon = 1.0e-6);
}
#[test]
fn test_adiabatic_taper_length_positive() {
let t = WaveguideTaper::new_adiabatic(300.0, 1000.0, 0.1, 2.45, 1.55e-6);
assert!(t.length_um > 0.0, "Adiabatic taper length must be positive");
}
#[test]
fn test_crossing_total_loss_mmi() {
let c = WaveguideCrossing::new_mmi_based(450.0);
let total = c.total_loss_db();
assert!(
total > 0.0 && total < 2.0,
"Unexpected crossing loss: {total} dB"
);
}
#[test]
fn test_router_route_has_segments() {
let router = PicRouter::new(1.0, 3.0, 5.0, 450.0);
let route = router.route([0.0, 0.0], 0.0, [100.0, 50.0], 180.0);
assert!(!route.segments.is_empty());
assert!(route.total_length_um > 0.0);
}
#[test]
fn test_router_loss_positive() {
let router = PicRouter::new(1.0, 3.0, 5.0, 450.0);
let route = router.route([0.0, 0.0], 0.0, [200.0, 100.0], 180.0);
let loss = router.route_loss_db(&route, 2.0, 0.05);
assert!(loss > 0.0, "Route loss must be positive");
}
}