use super::{FieldGain, Medium, NonlinearStep, StepRule, StepUpdate, manakov_rotation, mul, scale, signal_bandwidth};
use crate::fft;
use crate::{C64, cis};
use comnum_core::{Error, Result, ensure, same_len};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ManakovSplitStep {
pub medium: Medium,
pub rule: StepRule,
pub nonlinear: NonlinearStep,
pub max_iterations: usize,
pub tolerance: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct Birefringence<'a> {
pub section_km: f64,
pub rotations: &'a [[C64; 4]],
pub dgd_s_per_km: &'a [f64],
}
#[derive(Clone, Copy, Default)]
pub struct PropagationOptions<'a> {
pub gain: Option<FieldGain<'a>>,
pub birefringence: Option<Birefringence<'a>>,
}
impl std::fmt::Debug for PropagationOptions<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PropagationOptions")
.field("gain", &self.gain.map(|_| "FieldGain"))
.field("birefringence", &self.birefringence)
.finish()
}
}
struct Work {
omega: Vec<f64>,
p0: Vec<f64>,
p1: Vec<f64>,
phi: Vec<f64>,
lin: Vec<C64>,
half1: Vec<Vec<C64>>,
half2: Vec<Vec<C64>>,
gain: Vec<f64>,
h: Vec<Vec<C64>>,
f: Vec<Vec<C64>>,
c: Vec<Vec<C64>>,
}
impl ManakovSplitStep {
pub fn new(medium: Medium) -> ManakovSplitStep {
ManakovSplitStep {
medium,
rule: StepRule::FWM_DEFAULT,
nonlinear: NonlinearStep::Midpoint,
max_iterations: 10,
tolerance: 1e-5,
}
}
pub fn run(&self, x: &mut [C64], y: &mut [C64], length_km: f64, fs: f64) -> Result<usize> {
self.run_fields(&mut [x, y], length_km, fs, &PropagationOptions::default())
}
pub fn run_with_gain(
&self,
x: &mut [C64],
y: &mut [C64],
length_km: f64,
fs: f64,
gain: FieldGain<'_>,
) -> Result<usize> {
self.run_fields(&mut [x, y], length_km, fs, &PropagationOptions { gain: Some(gain), birefringence: None })
}
pub fn run_fields(
&self,
fields: &mut [&mut [C64]],
length_km: f64,
fs: f64,
options: &PropagationOptions<'_>,
) -> Result<usize> {
let nf = fields.len();
if nf == 0 || nf > 2 {
return Err(Error::Shape(format!("a propagation takes one or two fields, got {nf}")));
}
let n = fields[0].len();
if nf == 2 {
same_len(n, fields[1].len(), "the polarisations")?;
}
self.rule_parameter()?;
ensure(length_km.is_finite(), || format!("the length must be finite, got {length_km} km"))?;
if let Some(b) = &options.birefringence {
check_birefringence(b, nf, length_km)?;
}
if n == 0 || length_km <= 0.0 {
return Ok(0);
}
if let (StepRule::Fixed(hf), NonlinearStep::Midpoint) = (self.rule, self.nonlinear) {
return Ok(self.run_fixed_midpoint(fields, length_km, fs, options, hf));
}
let p = fft::plan(n);
let zero = C64::new(0.0, 0.0);
let mut w = Work {
omega: fft::angular_frequencies(n, fs),
p0: vec![0.0; n],
p1: vec![0.0; n],
phi: vec![0.0; n],
lin: vec![zero; n],
half1: vec![vec![zero; n]; nf],
half2: vec![vec![zero; n]; nf],
gain: vec![0.0; n],
h: vec![vec![zero; n]; nf],
f: vec![vec![zero; n]; nf],
c: fields.iter().map(|u| u.to_vec()).collect(),
};
let k = if nf == 2 { 8.0 / 9.0 } else { 1.0 } * self.medium.gamma_w_km;
let bandwidth = match self.rule {
StepRule::Fwm { bandwidth_hz: Some(b), .. } => b,
StepRule::Fwm { bandwidth_hz: None, .. } => {
let views: Vec<&[C64]> = fields.iter().map(|u| &**u).collect();
signal_bandwidth(&views, fs)
}
_ => 0.0,
};
let mut z = 0.0;
let mut steps = 0;
let mut previous = None;
let mut section: Option<usize> = None;
while z < length_km {
let mut stretch_end = length_km;
if let Some(b) = &options.birefringence {
let current = ((z / b.section_km) * (1.0 + 1e-12)).floor() as usize;
let current = current.min(b.rotations.len() - 1);
if section != Some(current) {
let (a, rest) = fields.split_at_mut(1);
rotate(&b.rotations[current], a[0], rest[0]);
let (a, rest) = w.c.split_at_mut(1);
rotate(&b.rotations[current], &mut a[0], &mut rest[0]);
section = Some(current);
}
stretch_end = (((current + 1) as f64) * b.section_km).min(length_km);
}
total_power_of(fields, &mut w.p0);
total_power_of(&w.c, &mut w.p1);
trapezoid(&w.p0, &w.p1, k, &mut w.phi);
let h = self.step_length(&w.phi, z, stretch_end - z, previous, bandwidth)?;
previous = Some(h);
self.half_steps(&mut w, z, h, section, options);
for (o, u) in w.h.iter_mut().zip(fields.iter()) {
o.copy_from_slice(u);
}
for (u, half) in w.h.iter_mut().zip(&w.half1) {
p.forward(u);
mul(u, half);
p.inverse(u);
}
if self.nonlinear == NonlinearStep::Midpoint {
kerr_rotation(&mut w.h, k * h);
for (u, half) in w.h.iter_mut().zip(&w.half2) {
p.forward(u);
mul(u, half);
p.inverse(u);
}
for ((u, c), hh) in fields.iter_mut().zip(w.c.iter_mut()).zip(&w.h) {
u.copy_from_slice(hh);
c.copy_from_slice(hh);
}
} else {
for it in 0..self.max_iterations.max(1) {
for (o, v) in w.f.iter_mut().zip(&w.h) {
for ((a, b), ph) in o.iter_mut().zip(v).zip(&w.phi) {
*a = b * cis(ph * h);
}
}
for (u, half) in w.f.iter_mut().zip(&w.half2) {
p.forward(u);
mul(u, half);
p.inverse(u);
}
let diff: f64 =
w.f.iter().zip(&w.c).flat_map(|(a, b)| a.iter().zip(b)).map(|(u, v)| (u - v).norm_sqr()).sum();
let norm: f64 = w.c.iter().flatten().map(C64::norm_sqr).sum();
std::mem::swap(&mut w.c, &mut w.f);
if norm == 0.0 || (diff / norm).sqrt() < self.tolerance || it + 1 == self.max_iterations {
break;
}
total_power_of(&w.c, &mut w.p1);
trapezoid(&w.p0, &w.p1, k, &mut w.phi);
}
for (u, c) in fields.iter_mut().zip(&w.c) {
u.copy_from_slice(c);
}
}
z += h;
steps += 1;
}
Ok(steps)
}
fn run_fixed_midpoint(
&self,
fields: &mut [&mut [C64]],
length_km: f64,
fs: f64,
options: &PropagationOptions<'_>,
hf: f64,
) -> usize {
let (nf, n) = (fields.len(), fields[0].len());
let p = fft::plan(n);
let zero = C64::new(0.0, 0.0);
let mut w = Work {
omega: fft::angular_frequencies(n, fs),
p0: Vec::new(),
p1: Vec::new(),
phi: Vec::new(),
lin: vec![zero; n],
half1: vec![vec![zero; n]; nf],
half2: vec![vec![zero; n]; nf],
gain: vec![0.0; n],
h: fields.iter().map(|u| u.to_vec()).collect(),
f: Vec::new(),
c: Vec::new(),
};
let k = if nf == 2 { 8.0 / 9.0 } else { 1.0 } * self.medium.gamma_w_km;
let (n_stretches, stretch_km) = match &options.birefringence {
Some(b) => (b.rotations.len(), b.section_km),
None => (1, length_km),
};
for u in &mut w.h {
p.forward(u);
}
let mut steps = 0;
for s in 0..n_stretches {
let z_start = s as f64 * stretch_km;
let z_end = if s + 1 == n_stretches { length_km } else { (s + 1) as f64 * stretch_km };
if let Some(b) = &options.birefringence {
let (a, rest) = w.h.split_at_mut(1);
rotate(&b.rotations[s], &mut a[0], &mut rest[0]);
}
let (n_steps, h) = equal_steps(z_end - z_start, hf);
let section = options.birefringence.is_some().then_some(s);
self.half_steps(&mut w, z_start, h, section, &PropagationOptions { gain: None, ..*options });
for i in 0..n_steps {
let z = z_start + i as f64 * h;
for (u, half) in w.h.iter_mut().zip(&w.half1) {
mul(u, half);
}
if let Some(g) = options.gain {
g(z, z + h / 2.0, &mut w.gain);
for u in &mut w.h {
scale(u, &w.gain);
}
}
for u in &mut w.h {
p.inverse(u);
}
kerr_rotation_scalar_order(&mut w.h, k, h);
for (u, half) in w.h.iter_mut().zip(&w.half2) {
p.forward(u);
mul(u, half);
}
if let Some(g) = options.gain {
g(z + h / 2.0, z + h, &mut w.gain);
for u in &mut w.h {
scale(u, &w.gain);
}
}
steps += 1;
}
}
for (u, spectrum) in fields.iter_mut().zip(w.h.iter_mut()) {
p.inverse(spectrum);
u.copy_from_slice(spectrum);
}
steps
}
fn half_steps(&self, w: &mut Work, z: f64, h: f64, section: Option<usize>, options: &PropagationOptions<'_>) {
self.medium.half_step_into(&w.omega, h, &mut w.lin);
let nf = w.half1.len();
let dgd = match (options.birefringence, section) {
(Some(b), Some(s)) => b.dgd_s_per_km[s],
_ => 0.0,
};
for i in 0..nf {
let sign = if i == 0 { -1.0 } else { 1.0 };
for ((a, b), (l, wk)) in w.half1[i].iter_mut().zip(w.half2[i].iter_mut()).zip(w.lin.iter().zip(&w.omega)) {
let d = if dgd == 0.0 { *l } else { l * cis(sign * 0.25 * wk * dgd * h) };
*a = d;
*b = d;
}
}
if let Some(g) = options.gain {
for (half, z0, z1) in [(&mut w.half1, z, z + h / 2.0), (&mut w.half2, z + h / 2.0, z + h)] {
g(z0, z1, &mut w.gain);
for v in half.iter_mut() {
for (o, b) in v.iter_mut().zip(&w.gain) {
*o *= b;
}
}
}
}
}
fn rule_parameter(&self) -> Result<f64> {
let (step, what) = match self.rule {
StepRule::Fixed(h) => (h, "fixed step (km)"),
StepRule::NonlinearPhase(r) => (r, "nonlinear rotation (rad)"),
StepRule::Fwm { phase_rad, bandwidth_hz, max_nl_phase_rad, .. } => {
if let Some(b) = bandwidth_hz {
ensure(b > 0.0 && b.is_finite(), || format!("the bandwidth must be positive, got {b} Hz"))?;
}
if let Some(r) = max_nl_phase_rad {
ensure(r > 0.0, || format!("the nonlinear rotation must be positive, got {r}"))?;
}
(phase_rad, "four-wave-mixing phase (rad)")
}
};
ensure(step > 0.0, || format!("the {what} must be positive, got {step}"))?;
Ok(step)
}
fn fwm_first_step(&self, phase_rad: f64, b: f64) -> f64 {
let w = 2.0 * std::f64::consts::PI * b;
let h = phase_rad / (self.medium.beta2_s2_km.abs() * w * w);
if h.is_finite() { h } else { f64::INFINITY }
}
pub(super) fn step_length(
&self,
phi: &[f64],
z: f64,
rest: f64,
previous: Option<f64>,
bandwidth: f64,
) -> Result<f64> {
let h = match self.rule {
StepRule::Fixed(hf) => equal_steps(rest, hf).1,
StepRule::NonlinearPhase(rot) => {
let m = phi.iter().copied().fold(0.0, f64::max);
if m > 0.0 && rest >= rot / m { rot / m } else { rest }
}
StepRule::Fwm { phase_rad, update, max_nl_phase_rad, .. } => {
let a = self.medium.alpha_np_km;
let h = match previous {
None => self.fwm_first_step(phase_rad, bandwidth),
Some(hk) if a <= 0.0 => hk,
Some(hk) => match update {
StepUpdate::ConstantLocalError => hk * (a * hk / 3.0).exp(),
StepUpdate::NonlinearPhase => {
let l = (1.0 - (-a * hk).exp()) / a * (a * hk).exp();
if a * l < 1.0 { -(1.0 - a * l).ln() / a } else { f64::INFINITY }
}
},
};
let m = phi.iter().copied().fold(0.0, f64::max);
let nl = match max_nl_phase_rad {
Some(rot) if m > 0.0 => rot / m,
_ => f64::INFINITY,
};
h.min(nl).min(rest)
}
};
if z + h <= z || h.is_nan() {
return Err(Error::Numerical(format!("the split step vanishes at z = {z} km (step {h} km)")));
}
Ok(h)
}
}
fn equal_steps(rest: f64, hf: f64) -> (usize, f64) {
let ratio = rest / hf;
let whole = ratio.round();
if whole >= 1.0 && (whole - ratio).abs() <= 1e-9 * ratio {
(whole as usize, hf)
} else {
let n = ratio.ceil().max(1.0);
(n as usize, rest / n)
}
}
fn check_birefringence(b: &Birefringence<'_>, nf: usize, length_km: f64) -> Result<()> {
if nf != 2 {
return Err(Error::Shape("a birefringence needs two polarisations".into()));
}
let n = b.rotations.len();
let covers = b.section_km > 0.0 && ((n as f64) * b.section_km - length_km).abs() <= 1e-9 * length_km.max(1.0);
if n == 0 || b.dgd_s_per_km.len() != n || !covers {
return Err(Error::Shape(format!(
"{n} sections of {} km and {} DGDs for a length of {length_km} km",
b.section_km,
b.dgd_s_per_km.len()
)));
}
Ok(())
}
fn rotate(m: &[C64; 4], x: &mut [C64], y: &mut [C64]) {
for (u, v) in x.iter_mut().zip(y.iter_mut()) {
let (a, b) = (m[0] * *u + m[1] * *v, m[2] * *u + m[3] * *v);
*u = a;
*v = b;
}
}
fn total_power_of<T: AsRef<[C64]>>(fields: &[T], out: &mut [f64]) {
out.fill(0.0);
for f in fields {
for (o, v) in out.iter_mut().zip(f.as_ref()) {
*o += v.norm_sqr();
}
}
}
fn kerr_rotation_scalar_order(fields: &mut [Vec<C64>], k: f64, h: f64) {
match fields {
[x] => {
for v in x.iter_mut() {
*v *= cis(k * v.norm_sqr() * h);
}
}
[x, y] => manakov_rotation(x, y, k * h),
_ => {}
}
}
fn kerr_rotation(fields: &mut [Vec<C64>], k: f64) {
match fields {
[x] => {
for v in x.iter_mut() {
*v *= cis(k * v.norm_sqr());
}
}
[x, y] => manakov_rotation(x, y, k),
_ => {}
}
}
fn trapezoid(p0: &[f64], p1: &[f64], k: f64, phi: &mut [f64]) {
for (f, (u, v)) in phi.iter_mut().zip(p0.iter().zip(p1)) {
*f = k * (u + v) / 2.0;
}
}