use crate::fft::{self, Fft};
use crate::{C64, cis};
use comnum_core::{Error, Result};
use num_complex::Complex32 as C32;
mod manakov;
pub use manakov::{Birefringence, ManakovSplitStep, PropagationOptions};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Medium {
pub alpha_np_km: f64,
pub beta2_s2_km: f64,
pub gamma_w_km: f64,
pub beta3_s3_km: f64,
}
impl Default for Medium {
fn default() -> Medium {
Medium { alpha_np_km: 0.0, beta2_s2_km: 0.0, gamma_w_km: 0.0, beta3_s3_km: 0.0 }
}
}
impl Medium {
pub fn negated(&self) -> Medium {
Medium {
alpha_np_km: -self.alpha_np_km,
beta2_s2_km: -self.beta2_s2_km,
gamma_w_km: -self.gamma_w_km,
beta3_s3_km: -self.beta3_s3_km,
}
}
pub fn linear_response(&self, omega: &[f64], length_km: f64) -> Vec<C64> {
let mut out = vec![C64::new(0.0, 0.0); omega.len()];
self.linear_response_into(omega, length_km, &mut out);
out
}
fn linear_response_into(&self, omega: &[f64], length_km: f64, out: &mut [C64]) {
let (a, b2, b3) = (self.alpha_np_km, self.beta2_s2_km, self.beta3_s3_km);
let g = (-(a / 2.0) * length_km).exp();
if b3 == 0.0 {
for (o, wk) in out.iter_mut().zip(omega) {
*o = g * cis(b2 / 2.0 * wk * wk * length_km);
}
} else {
for (o, wk) in out.iter_mut().zip(omega) {
*o = g * cis((b2 / 2.0 + b3 / 6.0 * wk) * wk * wk * length_km);
}
}
}
fn half_step(&self, omega: &[f64], h: f64) -> Vec<C64> {
self.linear_response(omega, h / 2.0)
}
fn half_step_into(&self, omega: &[f64], h: f64, out: &mut [C64]) {
self.linear_response_into(omega, h / 2.0, out);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Steps {
pub n_steps: usize,
pub step_km: f64,
pub nl_length_km: f64,
}
impl Steps {
pub fn new(n_steps: usize, step_km: f64) -> Steps {
Steps { n_steps, step_km, nl_length_km: step_km }
}
pub fn length_km(&self) -> f64 {
self.n_steps as f64 * self.step_km
}
}
#[derive(Clone, Debug)]
pub struct SplitStep {
lins: Vec<Vec<C64>>,
schedule: Vec<(usize, f64, f64)>,
plan: Option<Fft>,
gamma_w_km: f64,
steps: Steps,
}
impl SplitStep {
pub fn new(medium: &Medium, steps: Steps, n: usize, fs: f64) -> SplitStep {
let omega = fft::angular_frequencies(n, fs);
SplitStep {
lins: vec![medium.half_step(&omega, steps.step_km)],
schedule: vec![(0, steps.step_km, steps.nl_length_km); steps.n_steps],
plan: (n > 0).then(|| fft::plan(n)),
gamma_w_km: medium.gamma_w_km,
steps,
}
}
pub fn with_lengths(medium: &Medium, steps: &[(f64, f64)], n: usize, fs: f64) -> SplitStep {
let omega = fft::angular_frequencies(n, fs);
let mut lengths: Vec<f64> = Vec::new();
let mut lins = Vec::new();
let mut schedule = Vec::with_capacity(steps.len());
for &(h, nl) in steps {
let i = lengths.iter().position(|&l| l == h).unwrap_or_else(|| {
lengths.push(h);
lins.push(medium.half_step(&omega, h));
lengths.len() - 1
});
schedule.push((i, h, nl));
}
let k = steps.len().max(1) as f64;
let total: f64 = steps.iter().map(|s| s.0).sum();
let total_nl: f64 = steps.iter().map(|s| s.1).sum();
SplitStep {
lins,
schedule,
plan: (n > 0).then(|| fft::plan(n)),
gamma_w_km: medium.gamma_w_km,
steps: Steps { n_steps: steps.len(), step_km: total / k, nl_length_km: total_nl / k },
}
}
pub fn steps(&self) -> Steps {
self.steps
}
pub fn run(&self, e: &mut [C64]) {
let Some(p) = self.plan.as_ref() else {
assert!(e.is_empty(), "a field for an empty block");
return;
};
let g = self.gamma_w_km;
p.forward(e);
for &(i, _, h) in &self.schedule {
mul(e, &self.lins[i]);
p.inverse(e);
for v in e.iter_mut() {
*v *= cis(g * v.norm_sqr() * h);
}
p.forward(e);
mul(e, &self.lins[i]);
}
p.inverse(e);
}
pub fn run_dual(&self, x: &mut [C64], y: &mut [C64]) {
assert_eq!(x.len(), y.len(), "the polarisations must have the same length");
let Some(p) = self.plan.as_ref() else {
assert!(x.is_empty(), "a field for an empty block");
return;
};
p.forward(x);
p.forward(y);
for &(i, _, h) in &self.schedule {
mul(x, &self.lins[i]);
mul(y, &self.lins[i]);
p.inverse(x);
p.inverse(y);
manakov_rotation(x, y, 8.0 / 9.0 * self.gamma_w_km * h);
p.forward(x);
p.forward(y);
mul(x, &self.lins[i]);
mul(y, &self.lins[i]);
}
p.inverse(x);
p.inverse(y);
}
pub fn run_with_gain(&self, e: &mut [C64], gain: FieldGain<'_>) {
let Some(p) = self.plan.as_ref() else {
assert!(e.is_empty(), "a field for an empty block");
return;
};
let g = self.gamma_w_km;
let mut buf = vec![0.0; e.len()];
let mut z0 = 0.0;
p.forward(e);
for (k, &(i, step, h)) in self.schedule.iter().enumerate() {
if self.lins.len() == 1 {
z0 = k as f64 * step;
}
mul(e, &self.lins[i]);
gain(z0, z0 + step / 2.0, &mut buf);
scale(e, &buf);
p.inverse(e);
for v in e.iter_mut() {
*v *= cis(g * v.norm_sqr() * h);
}
p.forward(e);
mul(e, &self.lins[i]);
gain(z0 + step / 2.0, z0 + step, &mut buf);
scale(e, &buf);
z0 += step;
}
p.inverse(e);
}
}
pub type FieldGain<'a> = &'a dyn Fn(f64, f64, &mut [f64]);
#[derive(Clone)]
pub struct SplitStepF32 {
lin: Vec<C32>,
lin_n: Vec<C32>,
plan: Option<fft::FftF32>,
gamma_h: f32,
steps: Steps,
}
impl std::fmt::Debug for SplitStepF32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SplitStepF32")
.field("n", &self.lin.len())
.field("gamma_h", &self.gamma_h)
.field("steps", &self.steps)
.finish_non_exhaustive()
}
}
impl SplitStepF32 {
pub fn new(medium: &Medium, steps: Steps, n: usize, fs: f64) -> SplitStepF32 {
let omega = fft::angular_frequencies(n, fs);
let half = medium.half_step(&omega, steps.step_km);
let inv_n = 1.0 / n.max(1) as f64;
let lin = half.iter().map(|v| C32::new(v.re as f32, v.im as f32)).collect();
let lin_n = half.iter().map(|v| v * inv_n).map(|v| C32::new(v.re as f32, v.im as f32)).collect();
SplitStepF32 {
lin,
lin_n,
plan: (n > 0).then(|| fft::plan_f32(n)),
gamma_h: (medium.gamma_w_km * steps.nl_length_km) as f32,
steps,
}
}
pub fn steps(&self) -> Steps {
self.steps
}
pub fn run(&self, e: &mut [C32]) {
let Some(p) = self.plan.as_ref() else {
assert!(e.is_empty(), "a field for an empty block");
return;
};
assert_eq!(e.len(), self.lin.len(), "FFT length mismatch");
let n = e.len();
p.forward(e);
for _ in 0..self.steps.n_steps {
e.iter_mut().zip(&self.lin_n).for_each(|(u, l)| *u *= l);
p.inverse_unnormalized(e);
for v in e.iter_mut() {
let (sn, cs) = (self.gamma_h * v.norm_sqr()).sin_cos();
*v *= C32::new(cs, sn);
}
p.forward(e);
e.iter_mut().zip(&self.lin).for_each(|(u, l)| *u *= l);
}
p.inverse_unnormalized(e);
let s = 1.0 / n as f32;
for v in e.iter_mut() {
*v *= s;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum StepRule {
Fixed(f64),
NonlinearPhase(f64),
Logarithmic(usize),
Fwm {
phase_rad: f64,
bandwidth_hz: Option<f64>,
update: StepUpdate,
max_nl_phase_rad: Option<f64>,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum StepUpdate {
#[default]
ConstantLocalError,
NonlinearPhase,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum StepDistribution {
#[default]
Uniform,
Logarithmic,
ModifiedLogarithmic(f64),
}
impl StepDistribution {
pub fn steps(self, alpha_np_km: f64, length_km: f64, n: usize) -> Result<Vec<f64>> {
match self {
StepDistribution::Uniform => logarithmic_steps(0.0, length_km, n),
StepDistribution::Logarithmic => logarithmic_steps(alpha_np_km, length_km, n),
StepDistribution::ModifiedLogarithmic(factor) => {
if !(factor > 0.0 && factor.is_finite()) {
return Err(Error::Config(format!("the adjusting factor must be positive, got {factor}")));
}
logarithmic_steps(alpha_np_km * factor, length_km, n)
}
}
}
}
impl std::str::FromStr for StepDistribution {
type Err = Error;
fn from_str(s: &str) -> Result<StepDistribution> {
match s.to_ascii_lowercase().as_str() {
"uniform" => Ok(StepDistribution::Uniform),
"logarithmic" => Ok(StepDistribution::Logarithmic),
"modified_logarithmic" => Ok(StepDistribution::ModifiedLogarithmic(0.4)),
_ => Err(Error::Config(format!(
"the step distribution must be uniform, logarithmic or modified_logarithmic, got {s:?}"
))),
}
}
}
pub(crate) fn logarithmic_boundary(alpha_np_km: f64, length_km: f64, n: usize, k: usize) -> f64 {
let a = alpha_np_km;
if k >= n {
return length_km;
}
if a * length_km < 1e-12 {
return length_km * k as f64 / n as f64;
}
let sigma = -(-a * length_km).exp_m1() / n as f64;
-(-(k as f64) * sigma).ln_1p() / a
}
pub fn logarithmic_steps(alpha_np_km: f64, length_km: f64, n: usize) -> Result<Vec<f64>> {
if n == 0 {
return Err(Error::Config("at least one step".into()));
}
if !(length_km > 0.0 && length_km.is_finite()) {
return Err(Error::Config(format!("the length must be positive and finite, got {length_km} km")));
}
if !(alpha_np_km >= 0.0 && alpha_np_km.is_finite()) {
return Err(Error::Config(format!("the attenuation must be non-negative and finite, got {alpha_np_km} Np/km")));
}
let z: Vec<f64> = (0..=n).map(|k| logarithmic_boundary(alpha_np_km, length_km, n, k)).collect();
Ok(z.windows(2).map(|w| w[1] - w[0]).collect())
}
impl StepRule {
pub const FWM_DEFAULT: StepRule = StepRule::Fwm {
phase_rad: 20.0,
bandwidth_hz: None,
update: StepUpdate::ConstantLocalError,
max_nl_phase_rad: Some(2e-2),
};
}
pub fn signal_bandwidth(fields: &[&[C64]], fs: f64) -> f64 {
let Some(n) = fields.first().map(|f| f.len()) else { return 0.0 };
if n == 0 {
return 0.0;
}
let mut psd = vec![0.0; n];
for f in fields {
for (p, v) in psd.iter_mut().zip(fft::fft(f)) {
*p += v.norm_sqr();
}
}
let peak = psd.iter().copied().fold(0.0, f64::max);
if peak == 0.0 {
return 0.0;
}
let signed = |k: usize| if k < n.div_ceil(2) { k as f64 } else { k as f64 - n as f64 };
let above: Vec<f64> = psd.iter().enumerate().filter(|(_, p)| **p > 1e-4 * peak).map(|(k, _)| signed(k)).collect();
let (lo, hi) = above.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &k| (a.min(k), b.max(k)));
(hi - lo + 1.0) * fs / n as f64
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum NonlinearStep {
#[default]
Midpoint,
Trapezoidal,
}
impl std::str::FromStr for NonlinearStep {
type Err = Error;
fn from_str(s: &str) -> Result<NonlinearStep> {
match s.to_ascii_lowercase().as_str() {
"trapezoidal" => Ok(NonlinearStep::Trapezoidal),
"midpoint" => Ok(NonlinearStep::Midpoint),
_ => Err(Error::Config(format!("the nonlinear step must be trapezoidal or midpoint, got {s:?}"))),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SamplingCheck {
pub bandwidth_hz: f64,
pub rate_hz: f64,
pub min_rate_hz: f64,
pub window_s: f64,
pub walkoff_s: f64,
}
impl SamplingCheck {
pub fn is_sufficient(&self) -> bool {
self.rate_hz >= self.min_rate_hz && self.window_s >= self.walkoff_s
}
pub fn ensure(&self) -> Result<()> {
if self.is_sufficient() {
return Ok(());
}
let mut what = Vec::new();
if self.rate_hz < self.min_rate_hz {
what.push(format!(
"the sampling rate {:.1} GHz is below 3/2 of the signal bandwidth {:.1} GHz ({:.1} GHz): the four-wave mixing aliases onto the signal (Musetti et al. 2018, Table I)",
self.rate_hz / 1e9,
self.bandwidth_hz / 1e9,
self.min_rate_hz / 1e9
));
}
if self.window_s < self.walkoff_s {
what.push(format!(
"the block of {:.2} ns is shorter than the walk-off {:.2} ns across the bandwidth over the link: the circular convolution wraps the channels (Musetti et al. 2018, Appendix B)",
self.window_s * 1e9,
self.walkoff_s * 1e9
));
}
Err(Error::Config(format!("insufficient sampling: {} (check_sampling: false to run anyway)", what.join("; "))))
}
}
pub fn sampling_check(fields: &[&[C64]], fs: f64, medium: &Medium, length_km: f64) -> SamplingCheck {
let b = signal_bandwidth(fields, fs);
let n = fields.first().map_or(0, |f| f.len()) as f64;
SamplingCheck {
bandwidth_hz: b,
rate_hz: fs,
min_rate_hz: 1.5 * b,
window_s: n / fs,
walkoff_s: medium.beta2_s2_km.abs() * 2.0 * std::f64::consts::PI * b * length_km,
}
}
pub fn manakov_rotation(x: &mut [C64], y: &mut [C64], k: f64) {
assert_eq!(x.len(), y.len(), "the polarisations must have the same length");
for (u, v) in x.iter_mut().zip(y.iter_mut()) {
let e = cis(k * (u.norm_sqr() + v.norm_sqr()));
*u *= e;
*v *= e;
}
}
fn mul(a: &mut [C64], b: &[C64]) {
for (u, v) in a.iter_mut().zip(b) {
*u *= v;
}
}
fn scale(a: &mut [C64], b: &[f64]) {
for (u, v) in a.iter_mut().zip(b) {
*u *= v;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_field_propagates_to_an_empty_field_instead_of_panicking() {
let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let steps = Steps { n_steps: 3, step_km: 1.0, nl_length_km: 0.8 };
let s = SplitStep::new(&m, steps, 0, 64e9);
let (mut x, mut y): (Vec<C64>, Vec<C64>) = (Vec::new(), Vec::new());
s.run(&mut x);
s.run_dual(&mut x, &mut y);
s.run_with_gain(&mut x, &|_, _, _| {});
SplitStepF32::new(&m, steps, 0, 64e9).run(&mut []);
assert!(x.is_empty() && y.is_empty() && s.steps() == steps);
assert!((steps.length_km() - 3.0).abs() < 1e-15);
}
#[test]
fn a_uniform_gain_of_the_loss_is_the_attenuation_of_the_medium() {
let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let lossless = Medium { alpha_np_km: 0.0, ..m };
let x: Vec<C64> = (0..256).map(|n| cis(0.3 * n as f64) * (1e-2 * (1 + n % 5) as f64)).collect();
let steps = Steps::new(40, 0.5);
let (mut a, mut b) = (x.clone(), x);
SplitStep::new(&m, steps, 256, 64e9).run(&mut a);
let loss = |z0: f64, z1: f64, out: &mut [f64]| out.fill((-(0.046 / 2.0) * (z1 - z0)).exp());
SplitStep::new(&lossless, steps, 256, 64e9).run_with_gain(&mut b, &loss);
let peak = a.iter().map(|v| v.norm()).fold(0.0, f64::max);
assert!(a.iter().zip(&b).all(|(u, v)| (u - v).norm() < 1e-13 * peak));
}
#[test]
fn lossless_split_step_conserves_energy() {
let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let mut e: Vec<C64> = (0..256).map(|n| cis(0.1 * n as f64) * (1e-2 * (n % 7) as f64)).collect();
let e0: f64 = e.iter().map(C64::norm_sqr).sum();
SplitStep::new(&m, Steps::new(80, 1.0), 256, 64e9).run(&mut e);
let e1: f64 = e.iter().map(C64::norm_sqr).sum();
assert!((e1 / e0 - 1.0).abs() < 1e-12);
}
fn dual_field(n: usize) -> (Vec<C64>, Vec<C64>) {
let w = |k: usize, f: f64, ph: f64| (2.0 * std::f64::consts::PI * f * k as f64 / n as f64 + ph).sin();
let x = (0..n)
.map(|k| C64::new(w(k, 61.0, 0.3), w(k, 23.0, 1.1)) * (0.07 * (1.0 + 0.5 * w(k, 7.0, 0.0))))
.collect();
let y = (0..n)
.map(|k| C64::new(w(k, 41.0, 2.0), w(k, 89.0, 0.4)) * (0.07 * (1.0 + 0.5 * w(k, 5.0, 1.0))))
.collect();
(x, y)
}
#[test]
fn manakov_with_one_polarisation_is_the_scalar_equation_with_8_9_gamma() {
let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let (x0, _) = dual_field(512);
let (mut x, mut y) = (x0.clone(), vec![C64::new(0.0, 0.0); 512]);
let s = ManakovSplitStep {
rule: StepRule::Fixed(0.01),
max_iterations: 20,
tolerance: 1e-12,
..ManakovSplitStep::new(m)
};
s.run(&mut x, &mut y, 20.0, 128e9).unwrap();
let mut e = x0;
let scalar = Medium { gamma_w_km: 8.0 / 9.0 * 1.3, ..m };
SplitStep::new(&scalar, Steps::new(2000, 0.01), 512, 128e9).run(&mut e);
let err = x.iter().zip(&e).map(|(u, v)| (u - v).norm()).fold(0.0, f64::max);
let peak = e.iter().map(|v| v.norm()).fold(0.0, f64::max);
assert!(err < 1e-4 * peak, "{}", err / peak);
assert!(y.iter().all(|v| v.norm() == 0.0));
}
#[test]
fn manakov_rejects_a_step_that_does_not_advance_instead_of_looping() {
let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
for rule in [
StepRule::Fixed(0.0),
StepRule::Fixed(-1.0),
StepRule::Fixed(f64::NAN),
StepRule::NonlinearPhase(0.0),
StepRule::NonlinearPhase(-2e-2),
] {
let (mut x, mut y) = dual_field(64);
let s = ManakovSplitStep { rule, ..ManakovSplitStep::new(m) };
assert!(matches!(s.run(&mut x, &mut y, 1.0, 256e9), Err(Error::Config(_))), "{rule:?}");
}
let s = ManakovSplitStep::new(m);
let (mut x, mut y) = dual_field(64);
assert!(matches!(s.run(&mut x, &mut y, f64::INFINITY, 256e9), Err(Error::Config(_))));
assert!(matches!(s.run(&mut x, &mut y[..32], 1.0, 256e9), Err(Error::Shape(_))));
assert_eq!(s.run(&mut x, &mut y, 0.0, 256e9).unwrap(), 0);
x[0] = C64::new(f64::INFINITY, 0.0);
assert!(matches!(s.run(&mut x, &mut y, 1.0, 256e9), Err(Error::Numerical(_))));
}
#[test]
fn manakov_conserves_the_total_energy_and_converges_with_the_step() {
let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let run = |rule| {
let (mut x, mut y) = dual_field(1024);
let s = ManakovSplitStep { rule, ..ManakovSplitStep::new(m) };
let steps = s.run(&mut x, &mut y, 80.0, 256e9).unwrap();
(x, y, steps)
};
let (x0, y0) = dual_field(1024);
let e0: f64 = x0.iter().chain(&y0).map(C64::norm_sqr).sum();
let (xa, ya, na) = run(StepRule::NonlinearPhase(2e-2));
let (xb, yb, nb) = run(StepRule::NonlinearPhase(2e-4));
let e1: f64 = xa.iter().chain(&ya).map(C64::norm_sqr).sum();
assert!((e1 / e0 - 1.0).abs() < 1e-10);
assert!(nb > 50 * na);
let err: f64 = xa.iter().zip(&xb).chain(ya.iter().zip(&yb)).map(|(u, v)| (u - v).norm_sqr()).sum();
assert!((err / e0).sqrt() < 1e-3, "{}", (err / e0).sqrt());
}
#[test]
fn the_fwm_rule_follows_eq_13_of_musetti_and_grows_with_the_loss() {
let (n, fs) = (1024, 1.024e12);
let tone =
|f: f64| -> Vec<C64> { (0..n).map(|k| cis(2.0 * std::f64::consts::PI * f * k as f64 / fs)).collect() };
let x: Vec<C64> = tone(-100e9).iter().zip(tone(100e9)).map(|(a, b)| a + b).collect();
let y = vec![C64::new(0.0, 0.0); n];
assert!((signal_bandwidth(&[&x, &y], fs) - 201e9).abs() < 1.0);
assert_eq!(signal_bandwidth(&[&y], fs), 0.0);
let m = Medium {
alpha_np_km: 0.2 / 10.0 * std::f64::consts::LN_10,
beta2_s2_km: -2.1683e-23,
gamma_w_km: 1.3,
beta3_s3_km: 0.0,
};
let s = ManakovSplitStep {
rule: StepRule::Fwm {
phase_rad: 20.0,
bandwidth_hz: Some(350e9),
update: StepUpdate::ConstantLocalError,
max_nl_phase_rad: None,
},
..ManakovSplitStep::new(m)
};
let h1 = s.step_length(&[0.0], 0.0, 100.0, 100.0, None, 350e9).unwrap();
assert!((h1 - 20.0 / (2.1683e-23 * (2.0 * std::f64::consts::PI * 350e9).powi(2))).abs() < 1e-12);
assert!((h1 - 0.190).abs() < 0.001, "{h1}");
let h2 = s.step_length(&[0.0], h1, 100.0, h1 + 100.0, Some(h1), 350e9).unwrap();
assert!((h2 / h1 - (m.alpha_np_km * h1 / 3.0).exp()).abs() < 1e-12);
let lossless = ManakovSplitStep { medium: Medium { alpha_np_km: 0.0, ..m }, ..s };
assert_eq!(lossless.step_length(&[0.0], h1, 100.0, h1 + 100.0, Some(h1), 350e9).unwrap(), h1);
let nlp = ManakovSplitStep {
rule: StepRule::Fwm {
phase_rad: 20.0,
bandwidth_hz: Some(350e9),
update: StepUpdate::NonlinearPhase,
max_nl_phase_rad: None,
},
..s
};
let h2n = nlp.step_length(&[0.0], h1, 100.0, h1 + 100.0, Some(h1), 350e9).unwrap();
let leff = |h: f64| (1.0 - (-m.alpha_np_km * h).exp()) / m.alpha_np_km;
assert!((leff(h2n) / leff(h1) - (m.alpha_np_km * h1).exp()).abs() < 1e-12);
assert!(h2n > h2);
for rule in [
StepRule::Fwm {
phase_rad: 0.0,
bandwidth_hz: None,
update: StepUpdate::ConstantLocalError,
max_nl_phase_rad: None,
},
StepRule::Fwm {
phase_rad: 20.0,
bandwidth_hz: Some(0.0),
update: StepUpdate::ConstantLocalError,
max_nl_phase_rad: None,
},
StepRule::Fwm {
phase_rad: 20.0,
bandwidth_hz: None,
update: StepUpdate::ConstantLocalError,
max_nl_phase_rad: Some(0.0),
},
] {
let (mut a, mut b) = (x.clone(), y.clone());
assert!(ManakovSplitStep { rule, ..s }.run(&mut a, &mut b, 1.0, fs).is_err());
}
}
#[test]
fn the_third_order_dispersion_adds_a_cubic_phase() {
let m = Medium { beta2_s2_km: -2.17e-23, beta3_s3_km: 1.3e-37, ..Medium::default() };
let omega = fft::angular_frequencies(64, 64e9);
let r = m.linear_response(&omega, 50.0);
for (v, w) in r.iter().zip(&omega) {
let phase = (m.beta2_s2_km / 2.0 * w * w + m.beta3_s3_km / 6.0 * w * w * w) * 50.0;
assert!((v - cis(phase)).norm() < 1e-12);
}
let back = m.negated().linear_response(&omega, 50.0);
assert!(r.iter().zip(&back).all(|(a, b)| ((a * b) - C64::new(1.0, 0.0)).norm() < 1e-12));
}
#[test]
fn one_field_is_the_scalar_split_step_and_fixed_steps_are_equal() {
let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let (x0, _) = dual_field(1024);
let fs = 256e9;
let mut scalar = x0.clone();
SplitStep::new(&m, Steps::new(4, 2.5), 1024, fs).run(&mut scalar);
let engine = ManakovSplitStep { rule: StepRule::Fixed(3.0), ..ManakovSplitStep::new(m) };
let mut x = x0.clone();
assert_eq!(engine.run_fields(&mut [&mut x], 10.0, fs, &PropagationOptions::default()).unwrap(), 4);
let e: f64 = scalar.iter().map(C64::norm_sqr).sum();
let d: f64 = x.iter().zip(&scalar).map(|(u, v)| (u - v).norm_sqr()).sum();
assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
}
#[test]
fn the_birefringence_of_the_sections_is_their_lumped_response() {
let m = Medium { alpha_np_km: 0.0, beta2_s2_km: 0.0, gamma_w_km: 0.0, beta3_s3_km: 0.0 };
let (n, fs) = (256, 256e9);
let (x0, y0) = dual_field(n);
let rot = |t: f64, p: f64| -> [C64; 4] {
let (s, c) = t.sin_cos();
[C64::new(c, 0.0), -cis(-p) * s, cis(p) * s, C64::new(c, 0.0)]
};
let rotations = [rot(0.3, 0.1), rot(1.1, -0.7), rot(-0.4, 2.0)];
let dgd = [3e-12, 1e-12, 2e-12];
let b = Birefringence { section_km: 2.0, rotations: &rotations, dgd_s_per_km: &dgd };
let engine = ManakovSplitStep { rule: StepRule::Fixed(0.7), ..ManakovSplitStep::new(m) };
let (mut x, mut y) = (x0.clone(), y0.clone());
let options = PropagationOptions { gain: None, birefringence: Some(b) };
let steps = engine.run_fields(&mut [&mut x, &mut y], 6.0, fs, &options).unwrap();
assert_eq!(steps, 9); let omega = fft::angular_frequencies(n, fs);
let (fx, fy) = (fft::fft(&x0), fft::fft(&y0));
let mut ex = vec![C64::new(0.0, 0.0); n];
let mut ey = ex.clone();
for k in 0..n {
let (mut a, mut c) = (fx[k], fy[k]);
for (r, t) in rotations.iter().zip(&dgd) {
(a, c) = (r[0] * a + r[1] * c, r[2] * a + r[3] * c);
let ph = 0.5 * omega[k] * t * 2.0;
(a, c) = (a * cis(-ph), c * cis(ph));
}
ex[k] = a;
ey[k] = c;
}
let (ex, ey) = (fft::ifft(&ex), fft::ifft(&ey));
let e: f64 = ex.iter().chain(&ey).map(C64::norm_sqr).sum();
let d: f64 = x.iter().zip(&ex).chain(y.iter().zip(&ey)).map(|(u, v)| (u - v).norm_sqr()).sum();
assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
let mut z = x0.clone();
assert!(engine.run_fields(&mut [&mut z], 6.0, fs, &options).is_err());
assert!(engine.run_fields(&mut [&mut x, &mut y], 5.0, fs, &options).is_err());
let (mut a, mut b2, mut c) = (x0.clone(), x0.clone(), x0);
assert!(engine.run_fields(&mut [&mut a, &mut b2, &mut c], 1.0, fs, &PropagationOptions::default()).is_err());
assert!(engine.run_fields(&mut [], 1.0, fs, &PropagationOptions::default()).is_err());
}
#[test]
fn the_midpoint_manakov_step_is_the_scalar_split_step() {
let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let (x0, _) = dual_field(1024);
let (h, n_steps, fs) = (0.5, 160, 256e9);
let mut scalar = x0.clone();
SplitStep::new(&m, Steps::new(n_steps, h), 1024, fs).run(&mut scalar);
let solver = ManakovSplitStep {
rule: StepRule::Fixed(h),
nonlinear: NonlinearStep::Midpoint,
..ManakovSplitStep::new(Medium { gamma_w_km: 9.0 / 8.0 * m.gamma_w_km, ..m })
};
let (mut x, mut y) = (x0, vec![C64::new(0.0, 0.0); 1024]);
assert_eq!(solver.run(&mut x, &mut y, n_steps as f64 * h, fs).unwrap(), n_steps);
let e: f64 = scalar.iter().map(C64::norm_sqr).sum();
let d: f64 = x.iter().zip(&scalar).map(|(u, v)| (u - v).norm_sqr()).sum();
assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
assert!(y.iter().all(|v| v.norm() == 0.0));
assert_eq!("Midpoint".parse::<NonlinearStep>().unwrap(), NonlinearStep::Midpoint);
assert!("euler".parse::<NonlinearStep>().is_err());
}
#[test]
fn fundamental_soliton_keeps_its_shape() {
let (t0, b2, gamma): (f64, f64, f64) = (10e-12, -20e-24, 1.0);
let p0 = b2.abs() / (gamma * t0 * t0);
let (n, fs) = (1024, 2e12);
let a0: Vec<C64> = (0..n)
.map(|k| {
let t = (k as f64 - n as f64 / 2.0) / fs;
C64::new(p0.sqrt() / (t / t0).cosh(), 0.0)
})
.collect();
let m = Medium { alpha_np_km: 0.0, beta2_s2_km: b2, gamma_w_km: gamma, beta3_s3_km: 0.0 };
let mut a = a0.clone();
SplitStep::new(&m, Steps::new(1000, 0.01), n, fs).run(&mut a);
let err = a.iter().zip(&a0).map(|(u, v)| (u.norm() - v.norm()).abs()).fold(0.0, f64::max);
assert!(err < 1e-3 * p0.sqrt(), "relative envelope error {}", err / p0.sqrt());
}
#[test]
fn logarithmic_steps_are_eq_6_of_bosco_and_check_their_inputs() {
let alpha = 0.25 / (10.0 * std::f64::consts::E.log10()); let (l, k) = (50.0, 50usize);
let h = logarithmic_steps(alpha, l, k).unwrap();
let af = alpha / 2.0;
let sigma = (1.0 - (-2.0 * af * l).exp()) / k as f64;
for (n, hn) in h.iter().enumerate() {
let n = (n + 1) as f64;
let bosco = -1.0 / (2.0 * af) * ((1.0 - n * sigma) / (1.0 - (n - 1.0) * sigma)).ln();
assert!((hn - bosco).abs() < 1e-10 * bosco, "step {n}: {hn} against {bosco}");
}
assert!((h.iter().sum::<f64>() - l).abs() < 1e-12 * l);
assert_eq!(logarithmic_steps(alpha, 80.0, 1).unwrap(), vec![80.0]);
assert!(logarithmic_steps(0.0, 80.0, 4).unwrap().iter().all(|&s| (s - 20.0).abs() < 1e-12));
assert!(logarithmic_steps(alpha, 80.0, 0).is_err());
assert!(logarithmic_steps(alpha, 0.0, 4).is_err());
assert!(logarithmic_steps(-1.0, 80.0, 4).is_err());
assert_eq!(StepDistribution::Uniform.steps(alpha, 80.0, 4).unwrap(), vec![20.0; 4]);
}
#[test]
fn the_logarithmic_rule_runs_the_steps_of_logarithmic_steps() {
let alpha = 0.2 / (10.0 * std::f64::consts::E.log10());
let m = Medium { alpha_np_km: alpha, beta2_s2_km: -21.683e-24, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
let (n, fs) = (512usize, 64e9);
let w = |k: usize, f: f64, ph: f64| (2.0 * std::f64::consts::PI * f * k as f64 / n as f64 + ph).sin();
let x0: Vec<C64> = (0..n).map(|k| C64::new(w(k, 31.0, 0.2), w(k, 17.0, 1.0)) * 0.05).collect();
let mut a = x0.clone();
let taken = ManakovSplitStep { rule: StepRule::Logarithmic(7), ..ManakovSplitStep::new(m) }
.run_fields(&mut [&mut a], 80.0, fs, &PropagationOptions::default())
.unwrap();
assert_eq!(taken, 7);
let steps: Vec<(f64, f64)> = logarithmic_steps(alpha, 80.0, 7).unwrap().iter().map(|&h| (h, h)).collect();
let mut b = x0.clone();
SplitStep::with_lengths(&m, &steps, n, fs).run(&mut b);
assert!(a.iter().zip(&b).all(|(u, v)| (u - v).norm() < 1e-12), "fast path against with_lengths");
let mut c = x0.clone();
let taken = ManakovSplitStep {
rule: StepRule::Logarithmic(7),
nonlinear: NonlinearStep::Trapezoidal,
..ManakovSplitStep::new(m)
}
.run_fields(&mut [&mut c], 80.0, fs, &PropagationOptions::default())
.unwrap();
assert_eq!(taken, 7);
let (mut d, mut e) = (x0.clone(), x0.clone());
SplitStep::new(&m, Steps::new(4, 20.0), n, fs).run(&mut d);
SplitStep::with_lengths(&m, &[(20.0, 20.0); 4], n, fs).run(&mut e);
assert_eq!(d, e);
assert!(
ManakovSplitStep { rule: StepRule::Logarithmic(0), ..ManakovSplitStep::new(m) }
.run_fields(&mut [&mut d], 80.0, fs, &PropagationOptions::default())
.is_err()
);
}
}
#[cfg(test)]
mod literature {
use super::*;
fn fictitious_fwm(alpha: f64, steps: &[f64], dbeta: f64) -> f64 {
let total = 1.0 - (-alpha * steps.iter().sum::<f64>()).exp();
let mut z = 0.0;
let mut sum = C64::new(0.0, 0.0);
for &h in steps {
let g = (1.0 - (-alpha * h).exp()) * (-alpha * z).exp();
sum += C64::from_polar(g, dbeta * z);
z += h;
}
sum.norm_sqr() / (total * total)
}
#[test]
fn bosco_2000_fig_3_logarithmic_steps_keep_the_fictitious_fwm_near_1_over_k() {
let alpha = 0.25 / (10.0 * std::f64::consts::E.log10());
let (l, k) = (50.0, 50usize);
let log = logarithmic_steps(alpha, l, k).unwrap();
let uniform = vec![l / k as f64; k];
let mut z = 0.0;
let gammas: Vec<f64> = log
.iter()
.map(|&h| {
let g = (1.0 - (-alpha * h).exp()) * (-alpha * z).exp();
z += h;
g
})
.collect();
assert!(gammas.iter().all(|g| (g - gammas[0]).abs() < 1e-12 * gammas[0]));
let grid: Vec<f64> = (0..3800).map(|i| 2.0 + i as f64 * 0.01).collect();
let eta_log: Vec<f64> = grid.iter().map(|&b| fictitious_fwm(alpha, &log, b)).collect();
let eta_uni: Vec<f64> = grid.iter().map(|&b| fictitious_fwm(alpha, &uniform, b)).collect();
let peak_uniform = fictitious_fwm(alpha, &uniform, 2.0 * std::f64::consts::PI / (l / k as f64));
let mean_log = eta_log.iter().sum::<f64>() / grid.len() as f64;
let max_log = eta_log.iter().copied().fold(0.0, f64::max);
let max_uni = eta_uni.iter().copied().fold(0.0, f64::max);
assert!((peak_uniform - 1.0).abs() < 1e-9, "the peak of equal steps {peak_uniform}");
assert!(max_uni > 0.99, "equal steps {max_uni}");
assert!(
(mean_log * k as f64 - 1.0).abs() < 0.2,
"the mean of the logarithmic steps {mean_log}, floor {}",
1.0 / k as f64
);
assert!(max_log < 0.2, "the largest of the logarithmic steps {max_log}");
}
#[test]
fn hager_2021_example_1_modified_logarithmic_steps() {
let alpha = 0.2 / (10.0 * std::f64::consts::E.log10());
let h = StepDistribution::ModifiedLogarithmic(0.4).steps(alpha, 100.0, 2).unwrap();
assert!((h[0] - 30.0).abs() < 1.0 && (h[1] - 70.0).abs() < 1.0, "{h:?}");
let bosco = StepDistribution::Logarithmic.steps(alpha, 100.0, 2).unwrap();
assert!((bosco[0] - 14.8).abs() < 0.1, "{bosco:?}");
assert_eq!(
"modified_logarithmic".parse::<StepDistribution>().unwrap(),
StepDistribution::ModifiedLogarithmic(0.4)
);
assert!(StepDistribution::ModifiedLogarithmic(0.0).steps(alpha, 100.0, 2).is_err());
}
}