#![cfg(feature = "interconnect")]
use num_complex::Complex64;
use std::f64::consts::PI;
const C_LIGHT: f64 = 2.997_924_58e8;
pub trait SiPhElement {
fn s_params(&self, freq_hz: &[f64]) -> Vec<[Complex64; 4]>;
}
#[derive(Debug, Clone)]
pub struct WaveguideSection {
pub length_um: f64,
pub loss_db_per_cm: f64,
pub group_index: f64,
pub dispersion_ps_per_nm_per_km: f64,
}
impl SiPhElement for WaveguideSection {
fn s_params(&self, freq_hz: &[f64]) -> Vec<[Complex64; 4]> {
let length_m = self.length_um * 1e-6;
let length_cm = self.length_um * 1e-4;
let loss_db = self.loss_db_per_cm * length_cm;
let amplitude = 10.0_f64.powf(-loss_db / 20.0);
freq_hz
.iter()
.map(|&f| {
let phi = 2.0 * PI * self.group_index * f / C_LIGHT * length_m;
let s21 = Complex64::from_polar(amplitude, -phi);
[
Complex64::new(0.0, 0.0), s21, s21, Complex64::new(0.0, 0.0), ]
})
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Splitter50_50;
impl SiPhElement for Splitter50_50 {
fn s_params(&self, freq_hz: &[f64]) -> Vec<[Complex64; 4]> {
let s21 = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0);
freq_hz
.iter()
.map(|_| {
[
Complex64::new(0.0, 0.0), s21, s21, Complex64::new(0.0, 0.0), ]
})
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct DirectionalCoupler {
pub coupling_ratio: f64,
pub excess_loss_db: f64,
}
impl SiPhElement for DirectionalCoupler {
fn s_params(&self, freq_hz: &[f64]) -> Vec<[Complex64; 4]> {
let kappa = self.coupling_ratio.clamp(0.0, 1.0);
let excess_amplitude = 10.0_f64.powf(-self.excess_loss_db / 20.0);
let through_amplitude = (1.0 - kappa).sqrt() * excess_amplitude;
let s21 = Complex64::new(through_amplitude, 0.0);
freq_hz
.iter()
.map(|_| {
[
Complex64::new(0.0, 0.0), s21, s21, Complex64::new(0.0, 0.0), ]
})
.collect()
}
}
impl DirectionalCoupler {
pub fn cross_port_loss_db(&self) -> f64 {
let kappa = self.coupling_ratio.clamp(1e-20, 1.0);
let power = kappa * 10.0_f64.powf(-self.excess_loss_db / 10.0);
-10.0 * power.max(1e-40).log10()
}
}
#[inline]
fn cascade_two(a: [Complex64; 4], b: [Complex64; 4]) -> [Complex64; 4] {
let [s11a, s21a, s12a, s22a] = a;
let [s11b, s21b, s12b, s22b] = b;
let denom = Complex64::new(1.0, 0.0) - s11b * s22a;
if denom.norm() < 1e-30 {
return [
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(1.0, 0.0),
];
}
let s21_tot = s21b * s21a / denom;
let s12_tot = s12a * s12b / denom;
let s11_tot = s11a + s21a * s12a * s11b / denom;
let s22_tot = s22b + s21b * s12b * s22a / denom;
[s11_tot, s21_tot, s12_tot, s22_tot]
}
pub struct SiPhLink {
elements: Vec<Box<dyn SiPhElement>>,
}
impl SiPhLink {
pub fn new() -> Self {
Self {
elements: Vec::new(),
}
}
pub fn push(mut self, el: impl SiPhElement + 'static) -> Self {
self.elements.push(Box::new(el));
self
}
pub fn cascade(&self, freq_hz: &[f64]) -> Vec<[Complex64; 4]> {
let n_freq = freq_hz.len();
if n_freq == 0 {
return Vec::new();
}
let identity = Complex64::new(1.0, 0.0);
let zero = Complex64::new(0.0, 0.0);
let mut result: Vec<[Complex64; 4]> = vec![[zero, identity, identity, zero]; n_freq];
for element in &self.elements {
let s = element.s_params(freq_hz);
for (i, &sp) in s.iter().enumerate() {
result[i] = cascade_two(result[i], sp);
}
}
result
}
pub fn insertion_loss_db(&self, freq_hz: &[f64]) -> Vec<f64> {
self.cascade(freq_hz)
.into_iter()
.map(|[_, s21, _, _]| {
let mag = s21.norm();
if mag < 1e-40 {
400.0 } else {
-20.0 * mag.log10()
}
})
.collect()
}
pub fn group_delay_ps(&self, freq_hz: &[f64]) -> Vec<f64> {
if freq_hz.len() < 2 {
return Vec::new();
}
let params = self.cascade(freq_hz);
let mut phi = vec![0.0_f64; freq_hz.len()];
phi[0] = params[0][1].arg(); for i in 1..freq_hz.len() {
let ratio = params[i][1] / params[i - 1][1];
let increment = if ratio.norm() < 1e-40 {
0.0
} else {
ratio.arg()
};
phi[i] = phi[i - 1] + increment;
}
let n = freq_hz.len();
let mut gd = vec![0.0_f64; n];
for i in 1..(n - 1) {
let dphi = phi[i + 1] - phi[i - 1];
let domega = 2.0 * PI * (freq_hz[i + 1] - freq_hz[i - 1]);
if domega.abs() > 0.0 {
gd[i] = -dphi / domega * 1e12; }
}
if n >= 2 {
let dphi0 = phi[1] - phi[0];
let domega0 = 2.0 * PI * (freq_hz[1] - freq_hz[0]);
if domega0.abs() > 0.0 {
gd[0] = -dphi0 / domega0 * 1e12;
}
let dphi_n = phi[n - 1] - phi[n - 2];
let domega_n = 2.0 * PI * (freq_hz[n - 1] - freq_hz[n - 2]);
if domega_n.abs() > 0.0 {
gd[n - 1] = -dphi_n / domega_n * 1e12;
}
}
gd
}
pub fn bandwidth_3db_hz(&self, freq_hz: &[f64]) -> Option<f64> {
if freq_hz.len() < 2 {
return None;
}
let params = self.cascade(freq_hz);
let powers: Vec<f64> = params.iter().map(|p| p[1].norm_sqr()).collect();
let peak = powers.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if peak <= 0.0 {
return None;
}
let half_peak = peak / 2.0;
let peak_idx = powers
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)?;
let right_idx = (peak_idx + 1..freq_hz.len()).find(|&i| powers[i] < half_peak)?;
let p_lo = powers[right_idx - 1];
let p_hi = powers[right_idx];
let f_lo = freq_hz[right_idx - 1];
let f_hi = freq_hz[right_idx];
if (p_hi - p_lo).abs() < 1e-40 {
return None;
}
let t = (half_peak - p_lo) / (p_hi - p_lo);
let f_3db_right = f_lo + t * (f_hi - f_lo);
let f_peak = freq_hz[peak_idx];
Some(2.0 * (f_3db_right - f_peak).abs())
}
pub fn n_elements(&self) -> usize {
self.elements.len()
}
}
impl Default for SiPhLink {
fn default() -> Self {
Self::new()
}
}
fn compose_s_matrices(a: [Complex64; 4], b: [Complex64; 4]) -> [Complex64; 4] {
let denom = Complex64::new(1.0, 0.0) - a[3] * b[0];
if denom.norm() < 1e-30 {
return [
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(1.0, 0.0),
];
}
[
a[0] + a[1] * a[2] * b[0] / denom, b[1] * a[1] / denom, a[2] * b[2] / denom, b[3] + b[1] * b[2] * a[3] / denom, ]
}
pub fn chip_to_chip_link_response(
stages: &[&SiPhLink],
freq_grid_hz: &[f64],
) -> Vec<[Complex64; 4]> {
if stages.is_empty() {
return freq_grid_hz
.iter()
.map(|_| {
[
Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), ]
})
.collect();
}
let mut acc = stages[0].cascade(freq_grid_hz);
for stage in &stages[1..] {
let next = stage.cascade(freq_grid_hz);
for (cur, &nxt) in acc.iter_mut().zip(next.iter()) {
*cur = compose_s_matrices(*cur, nxt);
}
}
acc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_link_zero_insertion_loss() {
let link = SiPhLink::new();
let freqs = [193.41e12_f64];
let il = link.insertion_loss_db(&freqs);
assert!(
il[0].abs() < 1e-9,
"empty link should have 0 dB IL, got {:.6}",
il[0]
);
}
#[test]
fn waveguide_10mm_at_3db_per_cm() {
let wg = WaveguideSection {
length_um: 10_000.0,
loss_db_per_cm: 3.0,
group_index: 4.2,
dispersion_ps_per_nm_per_km: 0.0,
};
let link = SiPhLink::new().push(wg);
let freqs = [193.41e12_f64];
let il = link.insertion_loss_db(&freqs);
assert!(
(il[0] - 3.0).abs() < 0.01,
"Expected 3 dB IL, got {:.4}",
il[0]
);
}
#[test]
fn splitter_3db_loss() {
let link = SiPhLink::new().push(Splitter50_50);
let freqs = [193.41e12_f64];
let il = link.insertion_loss_db(&freqs);
assert!(
(il[0] - 3.0103).abs() < 0.01,
"Splitter50_50 IL should be ~3.01 dB, got {:.4}",
il[0]
);
}
#[test]
fn directional_coupler_through_loss() {
let dc = DirectionalCoupler {
coupling_ratio: 0.5,
excess_loss_db: 0.0,
};
let link = SiPhLink::new().push(dc);
let freqs = [193.41e12_f64];
let il = link.insertion_loss_db(&freqs);
assert!(
(il[0] - 3.0103).abs() < 0.01,
"DirectionalCoupler IL should be ~3.01 dB, got {:.4}",
il[0]
);
}
}