use num_complex::Complex64;
use crate::fiber::nonlinear::spm::{fft_spm, ifft_spm};
#[derive(Debug, Clone, Copy)]
pub struct XpmChannel {
pub gamma: f64,
pub walk_off_per_m: f64,
pub alpha: f64,
pub length: f64,
}
impl XpmChannel {
pub fn new(gamma: f64, d12_ps_per_km: f64, alpha_db_per_km: f64, length_m: f64) -> Self {
let walk_off_per_m = d12_ps_per_km * 1e-12 / 1e3; let alpha = alpha_db_per_km * 1e-3 / (10.0 / std::f64::consts::LN_10);
Self {
gamma,
walk_off_per_m,
alpha,
length: length_m,
}
}
pub fn effective_length(&self) -> f64 {
if self.alpha < 1e-30 {
return self.length;
}
(1.0 - (-self.alpha * self.length).exp()) / self.alpha
}
pub fn walk_off_length(&self, pulse_duration_s: f64) -> f64 {
if self.walk_off_per_m.abs() < 1e-30 {
return f64::INFINITY;
}
pulse_duration_s / self.walk_off_per_m.abs()
}
pub fn xpm_phase_shift(&self, p2_watts: f64) -> f64 {
2.0 * self.gamma * p2_watts * self.effective_length()
}
pub fn max_xpm_chirp(&self, p2_peak_w: f64, pulse_duration_s: f64) -> f64 {
4.0 * self.gamma * p2_peak_w * self.effective_length()
/ (pulse_duration_s * (2.0 * std::f64::consts::E).sqrt())
}
pub fn apply_xpm(&self, ch1: &[[f64; 2]], ch2: &[[f64; 2]]) -> Vec<[f64; 2]> {
let l_eff = self.effective_length();
ch1.iter()
.zip(ch2.iter())
.map(|(&[r1, i1], &[r2, i2])| {
let intensity2 = r2 * r2 + i2 * i2;
let phase = 2.0 * self.gamma * intensity2 * l_eff;
let (s, c) = phase.sin_cos();
[r1 * c - i1 * s, r1 * s + i1 * c]
})
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct XpmCoeff {
n2: f64,
wavelength1: f64,
wavelength2: f64,
a_eff: f64,
}
impl XpmCoeff {
pub fn new(n2: f64, wavelength1: f64, wavelength2: f64, a_eff: f64) -> Self {
Self {
n2,
wavelength1,
wavelength2,
a_eff,
}
}
pub fn gamma1(&self) -> f64 {
2.0 * std::f64::consts::PI * self.n2 / (self.wavelength1 * self.a_eff)
}
pub fn gamma2(&self) -> f64 {
2.0 * std::f64::consts::PI * self.n2 / (self.wavelength2 * self.a_eff)
}
pub fn xpm_coeff(&self) -> f64 {
2.0 * self.gamma1()
}
pub fn group_velocity_mismatch(&self, beta1_1: f64, beta1_2: f64) -> f64 {
beta1_1 - beta1_2
}
}
#[derive(Debug, Clone)]
pub struct TwoChannelPropagation {
pub xpm: XpmCoeff,
pub fiber_length: f64,
pub n_steps: usize,
pub dz: f64,
pub beta2_1: f64,
pub beta2_2: f64,
pub alpha1: f64,
pub alpha2: f64,
}
impl TwoChannelPropagation {
pub fn new(xpm: XpmCoeff, fiber_length: f64, n_steps: usize) -> Self {
let dz = fiber_length / n_steps as f64;
Self {
xpm,
fiber_length,
n_steps,
dz,
beta2_1: 0.0,
beta2_2: 0.0,
alpha1: 0.0,
alpha2: 0.0,
}
}
pub fn with_dispersion_loss(
mut self,
beta2_1: f64,
beta2_2: f64,
alpha1: f64,
alpha2: f64,
) -> Self {
self.beta2_1 = beta2_1;
self.beta2_2 = beta2_2;
self.alpha1 = alpha1;
self.alpha2 = alpha2;
self
}
pub fn propagate(
&self,
a1: &[Complex64],
a2: &[Complex64],
dt: f64,
) -> (Vec<Complex64>, Vec<Complex64>) {
let mut a1 = a1.to_vec();
let mut a2 = a2.to_vec();
let g1 = self.xpm.gamma1();
let g2 = self.xpm.gamma2();
for _ in 0..self.n_steps {
xpm_nl_half_step(&mut a1, &a2, g1, self.dz / 2.0);
xpm_nl_half_step(&mut a2, &a1, g2, self.dz / 2.0);
if self.beta2_1 != 0.0 || self.alpha1 != 0.0 {
apply_linear_step(&mut a1, self.beta2_1, self.alpha1, self.dz, dt);
}
if self.beta2_2 != 0.0 || self.alpha2 != 0.0 {
apply_linear_step(&mut a2, self.beta2_2, self.alpha2, self.dz, dt);
}
xpm_nl_half_step(&mut a1, &a2, g1, self.dz / 2.0);
xpm_nl_half_step(&mut a2, &a1, g2, self.dz / 2.0);
}
(a1, a2)
}
pub fn xpm_phase_shift(&self, pump_energy: f64) -> f64 {
2.0 * self.xpm.gamma1() * pump_energy
}
}
fn xpm_nl_half_step(a: &mut [Complex64], b: &[Complex64], gamma: f64, dz: f64) {
for (ai, bi) in a.iter_mut().zip(b.iter()) {
let phi = gamma * (ai.norm_sqr() + 2.0 * bi.norm_sqr()) * dz;
*ai *= Complex64::new(0.0, phi).exp();
}
}
fn apply_linear_step(a: &mut [Complex64], beta2: f64, alpha: f64, dz: f64, dt: f64) {
use std::f64::consts::PI;
let n = a.len();
let mut spec = fft_spm(a);
let df = 1.0 / (n as f64 * dt);
for (m, s) in spec.iter_mut().enumerate() {
let freq = if m < n / 2 {
m as f64 * df
} else {
(m as f64 - n as f64) * df
};
let omega = 2.0 * PI * freq;
let loss = (-alpha / 2.0 * dz).exp();
let disp_phase = -beta2 / 2.0 * omega * omega * dz;
*s *= Complex64::new(0.0, disp_phase).exp() * loss;
}
let out = ifft_spm(&spec);
a.copy_from_slice(&out);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xpm_phase_shift_scales_with_power() {
let x = XpmChannel::new(1.3e-3, 3.0, 0.2, 80e3);
let p1 = x.xpm_phase_shift(1.0);
let p2 = x.xpm_phase_shift(2.0);
assert!((p2 - 2.0 * p1).abs() < 1e-10);
}
#[test]
fn xpm_walk_off_length_decreases_with_walkoff() {
let x1 = XpmChannel::new(1.3e-3, 1.0, 0.2, 80e3); let x2 = XpmChannel::new(1.3e-3, 10.0, 0.2, 80e3); let lw1 = x1.walk_off_length(1e-12);
let lw2 = x2.walk_off_length(1e-12);
assert!(lw1 > lw2, "Larger walk-off → shorter L_W");
}
#[test]
fn xpm_apply_preserves_amplitude() {
let x = XpmChannel::new(1.3e-3, 3.0, 0.2, 80e3);
let ch1 = vec![[1.0f64, 0.0]; 5];
let ch2 = vec![[0.5f64, 0.0]; 5];
let out = x.apply_xpm(&ch1, &ch2);
for &[r, i] in &out {
assert!((r * r + i * i - 1.0).abs() < 1e-10);
}
}
#[test]
fn xpm_max_chirp_positive() {
let x = XpmChannel::new(1.3e-3, 3.0, 0.2, 80e3);
let chirp = x.max_xpm_chirp(100e-3, 1e-12);
assert!(chirp > 0.0);
}
#[test]
fn xpm_coeff_is_twice_gamma1() {
let xc = XpmCoeff::new(2.6e-20, 1550e-9, 1310e-9, 80e-12);
let ratio = xc.xpm_coeff() / xc.gamma1();
assert!(
(ratio - 2.0).abs() < 1e-10,
"xpm_coeff should be 2×γ₁, got ratio={ratio}"
);
}
#[test]
fn xpm_coeff_gamma_positive() {
let xc = XpmCoeff::new(2.6e-20, 1550e-9, 1310e-9, 80e-12);
assert!(xc.gamma1() > 0.0);
assert!(xc.gamma2() > 0.0);
}
#[test]
fn xpm_phase_shift_sign() {
let xc = XpmCoeff::new(2.6e-20, 1550e-9, 1310e-9, 80e-12);
let prop = TwoChannelPropagation::new(xc, 1e3, 10);
let phi = prop.xpm_phase_shift(1e-12); assert!(phi > 0.0, "XPM phase shift should be positive, got {phi}");
}
#[test]
fn two_channel_power_conservation() {
let xc = XpmCoeff::new(2.6e-20, 1550e-9, 1310e-9, 80e-12);
let prop = TwoChannelPropagation::new(xc, 1e3, 50);
let n = 64;
let dt = 0.5e-12;
let t_center = (n as f64 - 1.0) / 2.0 * dt;
let a1: Vec<Complex64> = (0..n)
.map(|i| {
let t = i as f64 * dt - t_center;
Complex64::new((-t * t / (2.0 * (5e-12_f64).powi(2))).exp(), 0.0)
})
.collect();
let a2: Vec<Complex64> = (0..n)
.map(|i| {
let t = i as f64 * dt - t_center;
Complex64::new(0.5 * (-t * t / (2.0 * (5e-12_f64).powi(2))).exp(), 0.0)
})
.collect();
let power_in: f64 = a1.iter().chain(a2.iter()).map(|v| v.norm_sqr()).sum();
let (b1, b2) = prop.propagate(&a1, &a2, dt);
let power_out: f64 = b1.iter().chain(b2.iter()).map(|v| v.norm_sqr()).sum();
let rel_err = (power_out - power_in).abs() / power_in;
assert!(
rel_err < 1e-6,
"Two-channel power not conserved: rel_err={rel_err:.2e}"
);
}
}