use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct GratingCoupler {
pub period: f64,
pub n_eff: f64,
pub n_clad: f64,
pub fill_factor: f64,
pub n_periods: usize,
}
impl GratingCoupler {
pub fn new(period: f64, n_eff: f64, n_clad: f64, fill_factor: f64, n_periods: usize) -> Self {
Self {
period,
n_eff,
n_clad,
fill_factor,
n_periods,
}
}
pub fn design(n_eff: f64, n_clad: f64, theta_rad: f64, wavelength: f64) -> Option<Self> {
let denom = n_eff - n_clad * theta_rad.sin();
if denom <= 0.0 {
return None;
}
let period = wavelength / denom;
Some(Self::new(period, n_eff, n_clad, 0.5, 20))
}
pub fn coupling_angle(&self, wavelength: f64) -> Option<f64> {
let sin_theta = (self.n_eff - wavelength / self.period) / self.n_clad;
if sin_theta.abs() > 1.0 {
None } else {
Some(sin_theta.asin())
}
}
pub fn bandwidth_3db(&self, wavelength: f64) -> f64 {
let theta = self.coupling_angle(wavelength).unwrap_or(0.0);
wavelength * theta.cos() / (self.n_eff * self.n_periods as f64)
}
pub fn peak_efficiency_estimate(&self) -> f64 {
4.0 * self.fill_factor * (1.0 - self.fill_factor)
}
pub fn length(&self) -> f64 {
self.period * self.n_periods as f64
}
}
#[derive(Debug, Clone)]
pub struct ApodizedGratingCoupler {
pub n_eff: f64,
pub n_clad: f64,
pub wavelength: f64,
pub period: f64,
pub n_periods: usize,
pub n_group: f64,
}
impl ApodizedGratingCoupler {
pub fn new(n_eff: f64, n_clad: f64, wavelength: f64, period: f64, n_periods: usize) -> Self {
Self {
n_eff,
n_clad,
wavelength,
period,
n_periods,
n_group: n_eff + 0.3,
}
}
pub fn with_group_index(mut self, n_g: f64) -> Self {
self.n_group = n_g;
self
}
pub fn coupling_angle_deg(&self) -> f64 {
let sin_theta = self.n_eff - self.wavelength / self.period;
sin_theta.clamp(-1.0, 1.0).asin().to_degrees()
}
pub fn apodized_fill_factors(&self, target_profile: &[f64]) -> Vec<f64> {
if target_profile.is_empty() {
return Vec::new();
}
let max_val = target_profile.iter().cloned().fold(0.0_f64, f64::max);
if max_val <= 0.0 {
return vec![0.5; target_profile.len()];
}
target_profile
.iter()
.map(|&a| {
let a_norm = (a / max_val).clamp(0.0, 1.0);
let ff = 0.5 - 0.5 * (1.0 - a_norm * a_norm).max(0.0).sqrt();
ff.clamp(0.01, 0.99)
})
.collect()
}
pub fn coupling_efficiency(&self, ff: &[f64]) -> f64 {
if ff.is_empty() {
return 0.0;
}
let n = ff.len();
let l_total = self.period * n as f64;
let w0 = l_total / 2.0;
let positions: Vec<f64> = (0..n)
.map(|i| (i as f64 + 0.5) * self.period - l_total / 2.0)
.collect();
let amplitudes: Vec<f64> = ff.iter().map(|&f| 2.0 * (f * (1.0 - f)).sqrt()).collect();
let gaussian: Vec<f64> = positions
.iter()
.map(|&x| (-x * x / (w0 * w0)).exp())
.collect();
let norm_a = amplitudes.iter().map(|&v| v * v).sum::<f64>().sqrt();
let norm_g = gaussian.iter().map(|&v| v * v).sum::<f64>().sqrt();
if norm_a < 1e-30 || norm_g < 1e-30 {
return 0.0;
}
let overlap: f64 = amplitudes
.iter()
.zip(gaussian.iter())
.map(|(&a, &g)| a * g)
.sum::<f64>()
/ (norm_a * norm_g);
(overlap * overlap).clamp(0.0, 1.0)
}
pub fn bandwidth_nm(&self) -> f64 {
let l = self.period * self.n_periods as f64;
if l <= 0.0 || self.n_group <= 0.0 {
return 0.0;
}
let bw_m = self.wavelength * self.wavelength / (self.n_group * l);
bw_m * 1e9
}
pub fn length(&self) -> f64 {
self.period * self.n_periods as f64
}
}
#[derive(Debug, Clone)]
pub struct GratingArray2d {
pub nx: usize,
pub ny: usize,
pub period_x: f64,
pub period_y: f64,
pub wavelength: f64,
}
impl GratingArray2d {
pub fn new(nx: usize, ny: usize, period_x: f64, period_y: f64) -> Self {
Self {
nx,
ny,
period_x,
period_y,
wavelength: 1550e-9,
}
}
pub fn with_wavelength(mut self, wavelength: f64) -> Self {
self.wavelength = wavelength;
self
}
pub fn far_field_pattern(
&self,
weights: &[f64],
theta_max_deg: f64,
n_pts: usize,
) -> Vec<Vec<f64>> {
assert_eq!(
weights.len(),
self.nx * self.ny,
"weights length must equal nx*ny"
);
assert!(n_pts >= 2, "n_pts must be >= 2");
let k0 = 2.0 * PI / self.wavelength;
let theta_max = theta_max_deg.to_radians();
let angles: Vec<f64> = (0..n_pts)
.map(|i| -theta_max + 2.0 * theta_max * i as f64 / (n_pts - 1) as f64)
.collect();
let mut pattern = vec![vec![0.0_f64; n_pts]; n_pts];
for (ix, &tx) in angles.iter().enumerate() {
let sin_tx = tx.sin();
for (iy, &ty) in angles.iter().enumerate() {
let sin_ty = ty.sin();
let mut re = 0.0_f64;
let mut im = 0.0_f64;
for n in 0..self.ny {
for m in 0..self.nx {
let w = weights[n * self.nx + m];
let phase = k0
* (m as f64 * self.period_x * sin_tx
+ n as f64 * self.period_y * sin_ty);
re += w * phase.cos();
im += w * phase.sin();
}
}
pattern[ix][iy] = re * re + im * im;
}
}
pattern
}
pub fn beam_waist_estimate(&self) -> f64 {
let lx = self.nx as f64 * self.period_x;
let ly = self.ny as f64 * self.period_y;
let l = lx.max(ly);
if l <= 0.0 {
return f64::INFINITY;
}
l / 2.0
}
pub fn n_elements(&self) -> usize {
self.nx * self.ny
}
}
pub struct EfficiencyVsTilt;
impl EfficiencyVsTilt {
pub fn sweep(
coupler: &ApodizedGratingCoupler,
tilt_deg_range: (f64, f64),
n_pts: usize,
) -> Vec<(f64, f64)> {
assert!(n_pts >= 2, "n_pts must be >= 2");
let n = coupler.n_periods;
let gaussian_target: Vec<f64> = (0..n)
.map(|i| {
let t = (i as f64 - (n - 1) as f64 / 2.0) / (n as f64 / 4.0);
(-t * t).exp()
})
.collect();
let ff = coupler.apodized_fill_factors(&gaussian_target);
let eta_peak = coupler.coupling_efficiency(&ff);
let theta0_rad = coupler.coupling_angle_deg().to_radians();
let l = coupler.length();
let delta_theta_3db = if l > 0.0 && coupler.n_group > 0.0 {
coupler.wavelength / (coupler.n_group * l * theta0_rad.cos().max(0.01))
} else {
0.1_f64.to_radians()
};
let (t_min, t_max) = tilt_deg_range;
(0..n_pts)
.map(|i| {
let tilt_deg = t_min + (t_max - t_min) * i as f64 / (n_pts - 1) as f64;
let delta_theta = (tilt_deg - coupler.coupling_angle_deg()).to_radians();
let x = PI * delta_theta / delta_theta_3db;
let sinc = if x.abs() < 1e-12 { 1.0 } else { x.sin() / x };
let eta = eta_peak * sinc * sinc;
(tilt_deg, eta.clamp(0.0, 1.0))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grating_coupler_phase_match() {
let theta = 10.0_f64.to_radians();
let lambda = 1550e-9;
let n_eff = 2.4;
let n_clad = 1.0;
let gc = GratingCoupler::design(n_eff, n_clad, theta, lambda)
.expect("Grating design should succeed");
let theta_check = gc
.coupling_angle(lambda)
.expect("Should have valid coupling angle");
let err_deg = (theta_check.to_degrees() - theta.to_degrees()).abs();
assert!(err_deg < 0.001, "Angle error={err_deg:.4}°");
}
#[test]
fn grating_coupler_period_reasonable() {
let gc = GratingCoupler::design(2.4, 1.444, 10.0_f64.to_radians(), 1550e-9).unwrap();
let period_nm = gc.period * 1e9;
assert!(
period_nm > 400.0 && period_nm < 1500.0,
"Period={period_nm:.1} nm out of expected range"
);
}
#[test]
fn grating_coupler_beyond_critical_angle_returns_none() {
let gc = GratingCoupler::new(300e-9, 2.4, 1.0, 0.5, 20); let angle = gc.coupling_angle(1550e-9);
let _ = angle;
}
#[test]
fn apodized_grating_coupler_coupling_angle_reasonable() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let angle_deg = agc.coupling_angle_deg();
assert!(
angle_deg > -30.0 && angle_deg < 30.0,
"Coupling angle={angle_deg:.2}° out of expected range"
);
}
#[test]
fn apodized_fill_factors_length_matches_input() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let target: Vec<f64> = (0..20).map(|i| (i as f64).exp()).collect();
let ff = agc.apodized_fill_factors(&target);
assert_eq!(ff.len(), 20);
}
#[test]
fn apodized_fill_factors_in_valid_range() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let target: Vec<f64> = (0..20)
.map(|i| {
let t = (i as f64 - 9.5) / 5.0;
(-t * t).exp()
})
.collect();
let ff = agc.apodized_fill_factors(&target);
for &f in &ff {
assert!((0.0..=1.0).contains(&f), "Fill factor {f} out of [0,1]");
}
}
#[test]
fn apodized_coupling_efficiency_between_zero_and_one() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let target: Vec<f64> = (0..20)
.map(|i| {
let t = (i as f64 - 9.5) / 5.0;
(-t * t).exp()
})
.collect();
let ff = agc.apodized_fill_factors(&target);
let eta = agc.coupling_efficiency(&ff);
assert!(
(0.0..=1.0).contains(&eta),
"Efficiency={eta:.4} out of [0,1]"
);
}
#[test]
fn apodized_bandwidth_nm_positive() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let bw = agc.bandwidth_nm();
assert!(bw > 0.0, "Bandwidth={bw:.3} nm should be positive");
}
#[test]
fn apodized_bandwidth_nm_physically_reasonable() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let bw = agc.bandwidth_nm();
assert!(bw > 0.5 && bw < 200.0, "Bandwidth={bw:.2} nm unexpected");
}
#[test]
fn apodized_fill_factors_uniform_input() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 10);
let target = vec![1.0_f64; 10];
let ff = agc.apodized_fill_factors(&target);
let f0 = ff[0];
for &f in &ff {
assert!((f - f0).abs() < 1e-10, "Expected uniform fill factors");
}
}
#[test]
fn apodized_empty_profile_returns_empty() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let ff = agc.apodized_fill_factors(&[]);
assert!(ff.is_empty());
}
#[test]
fn grating_array_2d_far_field_size() {
let arr = GratingArray2d::new(4, 4, 2e-6, 2e-6).with_wavelength(1550e-9);
let weights = vec![1.0_f64; 16];
let ff = arr.far_field_pattern(&weights, 30.0, 11);
assert_eq!(ff.len(), 11);
assert_eq!(ff[0].len(), 11);
}
#[test]
fn grating_array_2d_far_field_nonnegative() {
let arr = GratingArray2d::new(4, 4, 2e-6, 2e-6).with_wavelength(1550e-9);
let weights = vec![1.0_f64; 16];
let ff = arr.far_field_pattern(&weights, 30.0, 11);
for row in &ff {
for &v in row {
assert!(v >= 0.0, "Negative intensity {v}");
}
}
}
#[test]
fn grating_array_2d_peak_at_broadside() {
let arr = GratingArray2d::new(8, 8, 2e-6, 2e-6).with_wavelength(1550e-9);
let weights = vec![1.0_f64; 64];
let n = 21;
let ff = arr.far_field_pattern(&weights, 45.0, n);
let centre = n / 2;
let peak = ff[centre][centre];
for row in &ff {
for &v in row {
assert!(v <= peak + 1e-6, "Off-centre value {v} > peak {peak}");
}
}
}
#[test]
fn grating_array_2d_beam_waist_positive() {
let arr = GratingArray2d::new(8, 8, 2e-6, 2e-6);
assert!(arr.beam_waist_estimate() > 0.0);
}
#[test]
fn grating_array_2d_n_elements() {
let arr = GratingArray2d::new(5, 7, 1e-6, 1e-6);
assert_eq!(arr.n_elements(), 35);
}
#[test]
fn efficiency_vs_tilt_length_correct() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let sweep = EfficiencyVsTilt::sweep(&agc, (-20.0, 20.0), 41);
assert_eq!(sweep.len(), 41);
}
#[test]
fn efficiency_vs_tilt_peak_near_bragg_angle() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let theta_bragg = agc.coupling_angle_deg();
let sweep = EfficiencyVsTilt::sweep(&agc, (theta_bragg - 15.0, theta_bragg + 15.0), 61);
let (_, eta_at_bragg) = sweep[30]; for &(_, eta) in &sweep {
assert!(
eta <= eta_at_bragg + 1e-9,
"Off-peak eta={eta} > peak {eta_at_bragg}"
);
}
}
#[test]
fn efficiency_vs_tilt_all_nonnegative() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let sweep = EfficiencyVsTilt::sweep(&agc, (-30.0, 30.0), 31);
for (_, eta) in sweep {
assert!(eta >= 0.0, "Negative efficiency");
}
}
#[test]
fn efficiency_vs_tilt_decreases_far_from_peak() {
let agc = ApodizedGratingCoupler::new(2.65, 1.444, 1550e-9, 630e-9, 20);
let theta0 = agc.coupling_angle_deg();
let sweep = EfficiencyVsTilt::sweep(&agc, (theta0, theta0 + 20.0), 41);
let etas: Vec<f64> = sweep.iter().map(|&(_, e)| e).collect();
let eta_peak = etas[0];
let eta_far = etas[40];
assert!(
eta_far < eta_peak * 0.5,
"Efficiency far from Bragg angle ({eta_far:.4}) should be < 50% of peak ({eta_peak:.4})"
);
}
}