use crate::error::{OxiPhotonError, Result};
const C_NM_THZ: f64 = 299_792.458;
#[derive(Debug, Clone)]
pub struct ArrayedWaveguideGrating {
pub n_channels: usize,
pub channel_spacing_nm: f64,
pub center_wavelength_nm: f64,
pub n_eff_array: f64,
pub n_g_array: f64,
pub delta_l_um: f64,
pub insertion_loss_db: f64,
pub crosstalk_db: f64,
pub n_waveguides: usize,
}
impl ArrayedWaveguideGrating {
#[allow(clippy::too_many_arguments)]
pub fn new(
n_channels: usize,
channel_spacing_nm: f64,
center_wavelength_nm: f64,
n_eff_array: f64,
n_g_array: f64,
delta_l_um: f64,
n_waveguides: usize,
insertion_loss_db: f64,
crosstalk_db: f64,
) -> Self {
Self {
n_channels,
channel_spacing_nm,
center_wavelength_nm,
n_eff_array,
n_g_array,
delta_l_um,
insertion_loss_db,
crosstalk_db,
n_waveguides,
}
}
pub fn c_band_100ghz(n_channels: usize) -> Self {
let channel_spacing_nm = 0.8;
Self::new(
n_channels,
channel_spacing_nm,
1550.0,
2.4, 4.2, {
let lambda = 1550.0_f64;
let n_g = 4.2_f64;
let n_eff = 2.4_f64;
let delta_lambda = channel_spacing_nm;
n_g * lambda * lambda / (n_eff * delta_lambda * 1_000.0)
},
100, 3.0, -30.0, )
}
pub fn channel_wavelengths_nm(&self) -> Vec<f64> {
let half = (self.n_channels as f64 - 1.0) / 2.0;
(0..self.n_channels)
.map(|i| self.center_wavelength_nm + (i as f64 - half) * self.channel_spacing_nm)
.collect()
}
pub fn channel_bandwidth_nm(&self) -> f64 {
self.channel_spacing_nm * 0.5
}
fn channel_sigma_nm(&self) -> f64 {
let fwhm = self.channel_bandwidth_nm();
fwhm / (2.0 * (2.0 * 2.0_f64.ln()).sqrt())
}
pub fn channel_transmission(&self, lambda_nm: f64, ch_idx: usize) -> f64 {
if ch_idx >= self.n_channels {
return 0.0;
}
let channels = self.channel_wavelengths_nm();
let lambda_ch = channels[ch_idx];
let sigma = self.channel_sigma_nm();
let delta = lambda_nm - lambda_ch;
let gaussian = (-delta * delta / (2.0 * sigma * sigma)).exp();
let il_factor = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
gaussian * il_factor
}
pub fn transmission_matrix(
&self,
lambda_start_nm: f64,
lambda_end_nm: f64,
n_pts: usize,
) -> Result<Vec<Vec<f64>>> {
if n_pts < 2 {
return Err(OxiPhotonError::NumericalError(
"n_pts must be >= 2".to_owned(),
));
}
if lambda_start_nm >= lambda_end_nm || lambda_start_nm <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"invalid wavelength range [{lambda_start_nm}, {lambda_end_nm}]"
)));
}
let step = (lambda_end_nm - lambda_start_nm) / (n_pts - 1) as f64;
let matrix = (0..self.n_channels)
.map(|ch| {
(0..n_pts)
.map(|i| {
let lam = lambda_start_nm + i as f64 * step;
self.channel_transmission(lam, ch)
})
.collect::<Vec<f64>>()
})
.collect();
Ok(matrix)
}
pub fn grating_order(&self) -> f64 {
let delta_l_nm = self.delta_l_um * 1_000.0;
self.n_eff_array * delta_l_nm / self.center_wavelength_nm
}
pub fn fsr_nm(&self) -> f64 {
let m = self.grating_order();
if m == 0.0 {
return f64::INFINITY;
}
let delta_l_nm = self.delta_l_um * 1_000.0;
self.center_wavelength_nm * self.center_wavelength_nm / (m * self.n_g_array * delta_l_nm)
}
pub fn routing_matrix_at(&self, lambda_nm: f64) -> Vec<Vec<f64>> {
let channels = self.channel_wavelengths_nm();
let n = self.n_channels;
let mut matrix = vec![vec![0.0_f64; n]; n];
for (input_port, row) in matrix.iter_mut().enumerate() {
for (output_ch, entry) in row.iter_mut().enumerate() {
let effective_lambda = lambda_nm - input_port as f64 * self.channel_spacing_nm;
let delta = effective_lambda - channels[output_ch];
let sigma = self.channel_sigma_nm();
let gaussian = (-delta * delta / (2.0 * sigma * sigma)).exp();
let il_factor = 10.0_f64.powf(-self.insertion_loss_db / 10.0);
*entry = gaussian * il_factor;
}
}
matrix
}
pub fn thermal_sensitivity_nm_per_k(&self, dn_dt: f64) -> f64 {
self.center_wavelength_nm * dn_dt / self.n_g_array
}
pub fn tuning_power_mw(&self, r_thermal: f64, dn_dt: f64) -> f64 {
let sensitivity = self.thermal_sensitivity_nm_per_k(dn_dt);
if sensitivity == 0.0 || r_thermal == 0.0 {
return f64::INFINITY;
}
let delta_t_needed = self.fsr_nm() / sensitivity;
delta_t_needed / r_thermal
}
}
#[derive(Debug, Clone)]
pub struct ItuGrid {
pub center_freq_thz: f64,
pub channel_spacing_ghz: f64,
pub n_channels: usize,
}
impl ItuGrid {
pub fn c_band_100ghz(n_channels: usize) -> Self {
Self {
center_freq_thz: 193.1,
channel_spacing_ghz: 100.0,
n_channels,
}
}
pub fn c_band_50ghz(n_channels: usize) -> Self {
Self {
center_freq_thz: 193.1,
channel_spacing_ghz: 50.0,
n_channels,
}
}
pub fn l_band_100ghz(n_channels: usize) -> Self {
Self {
center_freq_thz: 190.1,
channel_spacing_ghz: 100.0,
n_channels,
}
}
pub fn channel_frequencies_thz(&self) -> Vec<f64> {
let spacing_thz = self.channel_spacing_ghz * 1e-3; let half = (self.n_channels as f64 - 1.0) / 2.0;
(0..self.n_channels)
.map(|i| self.center_freq_thz + (i as f64 - half) * spacing_thz)
.collect()
}
pub fn channel_wavelengths_nm(&self) -> Vec<f64> {
self.channel_frequencies_thz()
.iter()
.map(|&f| Self::frequency_to_wavelength_nm(f))
.collect()
}
pub fn nearest_channel(&self, lambda_nm: f64) -> usize {
let freq_thz = Self::wavelength_to_frequency_thz(lambda_nm);
let spacing_thz = self.channel_spacing_ghz * 1e-3;
let half = (self.n_channels as f64 - 1.0) / 2.0;
let raw_idx = (freq_thz - self.center_freq_thz) / spacing_thz + half;
let idx = raw_idx.round() as isize;
idx.clamp(0, self.n_channels as isize - 1) as usize
}
pub fn frequency_to_wavelength_nm(freq_thz: f64) -> f64 {
C_NM_THZ / freq_thz
}
pub fn wavelength_to_frequency_thz(lambda_nm: f64) -> f64 {
C_NM_THZ / lambda_nm
}
}
#[cfg(test)]
mod tests {
use super::*;
fn standard_awg() -> ArrayedWaveguideGrating {
ArrayedWaveguideGrating::new(
8,
0.8,
1550.0,
2.4,
4.2,
{
let lambda = 1550.0_f64;
let n_eff = 2.4_f64;
let n_g = 4.2_f64;
let n_ch = 8_f64;
let delta_lambda = 0.8_f64; let delta_l_sq_nm2 = lambda.powi(3) / (n_eff * n_g * delta_lambda * n_ch);
delta_l_sq_nm2.sqrt() / 1_000.0 },
100,
3.0,
-30.0,
)
}
#[test]
fn test_awg_channel_wavelengths() {
let awg = standard_awg();
let channels = awg.channel_wavelengths_nm();
assert_eq!(channels.len(), 8, "Should have 8 channels");
for i in 1..channels.len() {
let spacing = (channels[i] - channels[i - 1]).abs();
assert!(
(spacing - awg.channel_spacing_nm).abs() / awg.channel_spacing_nm < 1e-10,
"Channel spacing mismatch at ch {i}: {spacing:.6} vs {:.6}",
awg.channel_spacing_nm
);
}
let center_ch = channels[channels.len() / 2];
assert!(
(center_ch - awg.center_wavelength_nm).abs() < awg.channel_spacing_nm,
"Center channel {center_ch:.4} not near design center {:.4}",
awg.center_wavelength_nm
);
}
#[test]
fn test_awg_fsr() {
let awg = standard_awg();
let fsr = awg.fsr_nm();
let expected_approx = awg.n_channels as f64 * awg.channel_spacing_nm;
let rel_err = (fsr - expected_approx).abs() / expected_approx;
assert!(
rel_err < 0.15,
"FSR {fsr:.4} nm should be close to {expected_approx:.4} nm (rel err {rel_err:.3})"
);
}
#[test]
fn test_awg_routing_matrix() {
let awg = standard_awg();
let channels = awg.channel_wavelengths_nm();
for (ch_idx, &lambda_ch) in channels.iter().enumerate() {
let t_target = awg.channel_transmission(lambda_ch, ch_idx);
if ch_idx > 0 {
let t_adj = awg.channel_transmission(lambda_ch, ch_idx - 1);
assert!(
t_target > t_adj,
"Channel {ch_idx} at λ={lambda_ch:.2}: target T={t_target:.4} should be > adjacent T={t_adj:.4}"
);
}
if ch_idx + 1 < channels.len() {
let t_adj = awg.channel_transmission(lambda_ch, ch_idx + 1);
assert!(
t_target > t_adj,
"Channel {ch_idx} at λ={lambda_ch:.2}: target T={t_target:.4} should be > adjacent T={t_adj:.4}"
);
}
}
}
#[test]
fn test_itu_grid_100ghz() {
let grid = ItuGrid::c_band_100ghz(40);
let wls = grid.channel_wavelengths_nm();
assert_eq!(wls.len(), 40);
let expected_spacing_nm = 1550.0_f64.powi(2) / 299_792.458 * 0.1;
for i in 1..wls.len() {
let spacing = (wls[i - 1] - wls[i]).abs(); assert!(
(spacing - expected_spacing_nm).abs() / expected_spacing_nm < 0.05,
"100 GHz channel spacing at 1550 nm: got {spacing:.4} nm, expected ~{expected_spacing_nm:.4} nm"
);
}
}
#[test]
fn test_wavelength_frequency_conversion() {
let lambda = 1550.0_f64;
let freq = ItuGrid::wavelength_to_frequency_thz(lambda);
let lambda_back = ItuGrid::frequency_to_wavelength_nm(freq);
assert!(
(lambda_back - lambda).abs() < 1e-9,
"Roundtrip conversion: {lambda} nm → {freq:.6} THz → {lambda_back:.10} nm"
);
assert!(
(freq - 193.41).abs() < 0.1,
"1550 nm should be ~193.41 THz, got {freq:.4} THz"
);
}
#[test]
fn test_itu_nearest_channel() {
let grid = ItuGrid::c_band_100ghz(40);
let wls = grid.channel_wavelengths_nm();
for (i, &wl) in wls.iter().enumerate() {
let nearest = grid.nearest_channel(wl);
assert_eq!(
nearest, i,
"Channel {i} at λ={wl:.4} nm should map to itself, got {nearest}"
);
}
}
#[test]
fn test_awg_grating_order_positive() {
let awg = standard_awg();
let m = awg.grating_order();
assert!(m > 0.0, "Grating order should be positive, got {m}");
assert!(
m > 1.0 && m < 10_000.0,
"Grating order out of typical range: {m}"
);
}
#[test]
fn test_awg_insertion_loss() {
let awg = standard_awg();
let channels = awg.channel_wavelengths_nm();
let t = awg.channel_transmission(channels[0], 0);
let il_factor = 10.0_f64.powf(-awg.insertion_loss_db / 10.0);
assert!(
(t - il_factor).abs() / il_factor < 1e-10,
"On-peak transmission should equal il_factor={il_factor:.4}, got {t:.4}"
);
}
#[test]
fn test_awg_transmission_matrix_shape() {
let awg = standard_awg();
let matrix = awg
.transmission_matrix(1546.0, 1554.0, 100)
.expect("matrix");
assert_eq!(
matrix.len(),
awg.n_channels,
"Matrix should have n_channels rows"
);
for row in &matrix {
assert_eq!(row.len(), 100, "Each row should have 100 wavelength points");
}
}
#[test]
fn test_itu_grid_50ghz_has_double_channels() {
let grid_100 = ItuGrid::c_band_100ghz(20);
let grid_50 = ItuGrid::c_band_50ghz(40);
let freqs_100 = grid_100.channel_frequencies_thz();
let freqs_50 = grid_50.channel_frequencies_thz();
let spacing_100 = (freqs_100[1] - freqs_100[0]).abs();
let spacing_50 = (freqs_50[1] - freqs_50[0]).abs();
assert!(
(spacing_100 / spacing_50 - 2.0).abs() < 1e-10,
"100 GHz spacing should be 2× 50 GHz spacing: {spacing_100} vs {spacing_50}"
);
}
#[test]
fn test_thermal_sensitivity() {
let awg = standard_awg();
let sens = awg.thermal_sensitivity_nm_per_k(1.86e-4);
assert!(sens > 0.0, "Thermal sensitivity should be positive: {sens}");
assert!(
sens > 0.01 && sens < 1.0,
"Thermal sensitivity out of range: {sens} nm/K"
);
}
}