1use crate::fft::{self, Fft};
81use crate::{C64, cis};
82use comnum_core::{Error, Result};
83use num_complex::Complex32 as C32;
84
85mod manakov;
86
87pub use manakov::{Birefringence, ManakovSplitStep, PropagationOptions};
88
89#[derive(Clone, Copy, Debug, PartialEq)]
91pub struct Medium {
92 pub alpha_np_km: f64,
94 pub beta2_s2_km: f64,
96 pub gamma_w_km: f64,
98 pub beta3_s3_km: f64,
100}
101
102impl Default for Medium {
103 fn default() -> Medium {
105 Medium { alpha_np_km: 0.0, beta2_s2_km: 0.0, gamma_w_km: 0.0, beta3_s3_km: 0.0 }
106 }
107}
108
109impl Medium {
110 pub fn negated(&self) -> Medium {
113 Medium {
114 alpha_np_km: -self.alpha_np_km,
115 beta2_s2_km: -self.beta2_s2_km,
116 gamma_w_km: -self.gamma_w_km,
117 beta3_s3_km: -self.beta3_s3_km,
118 }
119 }
120
121 pub fn linear_response(&self, omega: &[f64], length_km: f64) -> Vec<C64> {
124 let mut out = vec![C64::new(0.0, 0.0); omega.len()];
125 self.linear_response_into(omega, length_km, &mut out);
126 out
127 }
128
129 fn linear_response_into(&self, omega: &[f64], length_km: f64, out: &mut [C64]) {
131 let (a, b2, b3) = (self.alpha_np_km, self.beta2_s2_km, self.beta3_s3_km);
132 let g = (-(a / 2.0) * length_km).exp();
133 if b3 == 0.0 {
134 for (o, wk) in out.iter_mut().zip(omega) {
135 *o = g * cis(b2 / 2.0 * wk * wk * length_km);
136 }
137 } else {
138 for (o, wk) in out.iter_mut().zip(omega) {
139 *o = g * cis((b2 / 2.0 + b3 / 6.0 * wk) * wk * wk * length_km);
140 }
141 }
142 }
143
144 fn half_step(&self, omega: &[f64], h: f64) -> Vec<C64> {
146 self.linear_response(omega, h / 2.0)
147 }
148
149 fn half_step_into(&self, omega: &[f64], h: f64, out: &mut [C64]) {
151 self.linear_response_into(omega, h / 2.0, out);
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
161pub struct Steps {
162 pub n_steps: usize,
164 pub step_km: f64,
166 pub nl_length_km: f64,
168}
169
170impl Steps {
171 pub fn new(n_steps: usize, step_km: f64) -> Steps {
174 Steps { n_steps, step_km, nl_length_km: step_km }
175 }
176
177 pub fn length_km(&self) -> f64 {
179 self.n_steps as f64 * self.step_km
180 }
181}
182
183#[derive(Clone, Debug)]
185pub struct SplitStep {
186 lin: Vec<C64>,
187 plan: Option<Fft>,
189 gamma_w_km: f64,
190 steps: Steps,
191}
192
193impl SplitStep {
194 pub fn new(medium: &Medium, steps: Steps, n: usize, fs: f64) -> SplitStep {
197 let omega = fft::angular_frequencies(n, fs);
198 SplitStep {
199 lin: medium.half_step(&omega, steps.step_km),
200 plan: (n > 0).then(|| fft::plan(n)),
201 gamma_w_km: medium.gamma_w_km,
202 steps,
203 }
204 }
205
206 pub fn steps(&self) -> Steps {
208 self.steps
209 }
210
211 pub fn run(&self, e: &mut [C64]) {
216 let Some(p) = self.plan.as_ref() else {
217 assert!(e.is_empty(), "a field for an empty block");
218 return;
219 };
220 let (g, h) = (self.gamma_w_km, self.steps.nl_length_km);
221 p.forward(e);
222 for _ in 0..self.steps.n_steps {
223 mul(e, &self.lin);
224 p.inverse(e);
225 for v in e.iter_mut() {
226 *v *= cis(g * v.norm_sqr() * h);
227 }
228 p.forward(e);
229 mul(e, &self.lin);
230 }
231 p.inverse(e);
232 }
233
234 pub fn run_dual(&self, x: &mut [C64], y: &mut [C64]) {
243 assert_eq!(x.len(), y.len(), "the polarisations must have the same length");
244 let Some(p) = self.plan.as_ref() else {
245 assert!(x.is_empty(), "a field for an empty block");
246 return;
247 };
248 let k = 8.0 / 9.0 * self.gamma_w_km * self.steps.nl_length_km;
249 p.forward(x);
250 p.forward(y);
251 for _ in 0..self.steps.n_steps {
252 mul(x, &self.lin);
253 mul(y, &self.lin);
254 p.inverse(x);
255 p.inverse(y);
256 manakov_rotation(x, y, k);
257 p.forward(x);
258 p.forward(y);
259 mul(x, &self.lin);
260 mul(y, &self.lin);
261 }
262 p.inverse(x);
263 p.inverse(y);
264 }
265
266 pub fn run_with_gain(&self, e: &mut [C64], gain: FieldGain<'_>) {
277 let Some(p) = self.plan.as_ref() else {
278 assert!(e.is_empty(), "a field for an empty block");
279 return;
280 };
281 let (g, h) = (self.gamma_w_km, self.steps.nl_length_km);
282 let step = self.steps.step_km;
283 let mut buf = vec![0.0; e.len()];
284 p.forward(e);
285 for k in 0..self.steps.n_steps {
286 let z0 = k as f64 * step;
287 mul(e, &self.lin);
288 gain(z0, z0 + step / 2.0, &mut buf);
289 scale(e, &buf);
290 p.inverse(e);
291 for v in e.iter_mut() {
292 *v *= cis(g * v.norm_sqr() * h);
293 }
294 p.forward(e);
295 mul(e, &self.lin);
296 gain(z0 + step / 2.0, z0 + step, &mut buf);
297 scale(e, &buf);
298 }
299 p.inverse(e);
300 }
301}
302
303pub type FieldGain<'a> = &'a dyn Fn(f64, f64, &mut [f64]);
306
307#[derive(Clone)]
315pub struct SplitStepF32 {
316 lin: Vec<C32>,
317 lin_n: Vec<C32>,
318 plan: Option<fft::FftF32>,
320 gamma_h: f32,
321 steps: Steps,
322}
323
324impl std::fmt::Debug for SplitStepF32 {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 f.debug_struct("SplitStepF32")
327 .field("n", &self.lin.len())
328 .field("gamma_h", &self.gamma_h)
329 .field("steps", &self.steps)
330 .finish_non_exhaustive()
331 }
332}
333
334impl SplitStepF32 {
335 pub fn new(medium: &Medium, steps: Steps, n: usize, fs: f64) -> SplitStepF32 {
338 let omega = fft::angular_frequencies(n, fs);
339 let half = medium.half_step(&omega, steps.step_km);
340 let inv_n = 1.0 / n.max(1) as f64;
342 let lin = half.iter().map(|v| C32::new(v.re as f32, v.im as f32)).collect();
343 let lin_n = half.iter().map(|v| v * inv_n).map(|v| C32::new(v.re as f32, v.im as f32)).collect();
344 SplitStepF32 {
345 lin,
346 lin_n,
347 plan: (n > 0).then(|| fft::plan_f32(n)),
348 gamma_h: (medium.gamma_w_km * steps.nl_length_km) as f32,
349 steps,
350 }
351 }
352
353 pub fn steps(&self) -> Steps {
355 self.steps
356 }
357
358 pub fn run(&self, e: &mut [C32]) {
363 let Some(p) = self.plan.as_ref() else {
364 assert!(e.is_empty(), "a field for an empty block");
365 return;
366 };
367 assert_eq!(e.len(), self.lin.len(), "FFT length mismatch");
368 let n = e.len();
369 p.forward(e);
370 for _ in 0..self.steps.n_steps {
371 e.iter_mut().zip(&self.lin_n).for_each(|(u, l)| *u *= l);
372 p.inverse_unnormalized(e);
373 for v in e.iter_mut() {
374 let (sn, cs) = (self.gamma_h * v.norm_sqr()).sin_cos();
375 *v *= C32::new(cs, sn);
376 }
377 p.forward(e);
378 e.iter_mut().zip(&self.lin).for_each(|(u, l)| *u *= l);
379 }
380 p.inverse_unnormalized(e);
381 let s = 1.0 / n as f32;
382 for v in e.iter_mut() {
383 *v *= s;
384 }
385 }
386}
387
388#[derive(Clone, Copy, Debug, PartialEq)]
390#[non_exhaustive]
391pub enum StepRule {
392 Fixed(f64),
394 NonlinearPhase(f64),
396 Fwm {
409 phase_rad: f64,
411 bandwidth_hz: Option<f64>,
414 update: StepUpdate,
416 max_nl_phase_rad: Option<f64>,
420 },
421}
422
423#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum StepUpdate {
428 #[default]
431 ConstantLocalError,
432 NonlinearPhase,
436}
437
438impl StepRule {
439 pub const FWM_DEFAULT: StepRule = StepRule::Fwm {
446 phase_rad: 20.0,
447 bandwidth_hz: None,
448 update: StepUpdate::ConstantLocalError,
449 max_nl_phase_rad: Some(2e-2),
450 };
451}
452
453pub fn signal_bandwidth(fields: &[&[C64]], fs: f64) -> f64 {
457 let Some(n) = fields.first().map(|f| f.len()) else { return 0.0 };
458 if n == 0 {
459 return 0.0;
460 }
461 let mut psd = vec![0.0; n];
462 for f in fields {
463 for (p, v) in psd.iter_mut().zip(fft::fft(f)) {
464 *p += v.norm_sqr();
465 }
466 }
467 let peak = psd.iter().copied().fold(0.0, f64::max);
468 if peak == 0.0 {
469 return 0.0;
470 }
471 let signed = |k: usize| if k < n.div_ceil(2) { k as f64 } else { k as f64 - n as f64 };
473 let above: Vec<f64> = psd.iter().enumerate().filter(|(_, p)| **p > 1e-4 * peak).map(|(k, _)| signed(k)).collect();
474 let (lo, hi) = above.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &k| (a.min(k), b.max(k)));
475 (hi - lo + 1.0) * fs / n as f64
476}
477
478#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
480#[non_exhaustive]
481pub enum NonlinearStep {
482 #[default]
485 Midpoint,
486 Trapezoidal,
491}
492
493impl std::str::FromStr for NonlinearStep {
494 type Err = Error;
495
496 fn from_str(s: &str) -> Result<NonlinearStep> {
498 match s.to_ascii_lowercase().as_str() {
499 "trapezoidal" => Ok(NonlinearStep::Trapezoidal),
500 "midpoint" => Ok(NonlinearStep::Midpoint),
501 _ => Err(Error::Config(format!("the nonlinear step must be trapezoidal or midpoint, got {s:?}"))),
502 }
503 }
504}
505
506#[derive(Clone, Copy, Debug, PartialEq)]
514pub struct SamplingCheck {
515 pub bandwidth_hz: f64,
517 pub rate_hz: f64,
519 pub min_rate_hz: f64,
521 pub window_s: f64,
523 pub walkoff_s: f64,
525}
526
527impl SamplingCheck {
528 pub fn is_sufficient(&self) -> bool {
530 self.rate_hz >= self.min_rate_hz && self.window_s >= self.walkoff_s
531 }
532
533 pub fn ensure(&self) -> Result<()> {
538 if self.is_sufficient() {
539 return Ok(());
540 }
541 let mut what = Vec::new();
542 if self.rate_hz < self.min_rate_hz {
543 what.push(format!(
544 "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)",
545 self.rate_hz / 1e9,
546 self.bandwidth_hz / 1e9,
547 self.min_rate_hz / 1e9
548 ));
549 }
550 if self.window_s < self.walkoff_s {
551 what.push(format!(
552 "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)",
553 self.window_s * 1e9,
554 self.walkoff_s * 1e9
555 ));
556 }
557 Err(Error::Config(format!("insufficient sampling: {} (check_sampling: false to run anyway)", what.join("; "))))
558 }
559}
560
561pub fn sampling_check(fields: &[&[C64]], fs: f64, medium: &Medium, length_km: f64) -> SamplingCheck {
563 let b = signal_bandwidth(fields, fs);
564 let n = fields.first().map_or(0, |f| f.len()) as f64;
565 SamplingCheck {
566 bandwidth_hz: b,
567 rate_hz: fs,
568 min_rate_hz: 1.5 * b,
569 window_s: n / fs,
570 walkoff_s: medium.beta2_s2_km.abs() * 2.0 * std::f64::consts::PI * b * length_km,
571 }
572}
573
574pub fn manakov_rotation(x: &mut [C64], y: &mut [C64], k: f64) {
596 assert_eq!(x.len(), y.len(), "the polarisations must have the same length");
597 for (u, v) in x.iter_mut().zip(y.iter_mut()) {
598 let e = cis(k * (u.norm_sqr() + v.norm_sqr()));
599 *u *= e;
600 *v *= e;
601 }
602}
603
604fn mul(a: &mut [C64], b: &[C64]) {
605 for (u, v) in a.iter_mut().zip(b) {
606 *u *= v;
607 }
608}
609
610fn scale(a: &mut [C64], b: &[f64]) {
611 for (u, v) in a.iter_mut().zip(b) {
612 *u *= v;
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn an_empty_field_propagates_to_an_empty_field_instead_of_panicking() {
622 let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
623 let steps = Steps { n_steps: 3, step_km: 1.0, nl_length_km: 0.8 };
624 let s = SplitStep::new(&m, steps, 0, 64e9);
625 let (mut x, mut y): (Vec<C64>, Vec<C64>) = (Vec::new(), Vec::new());
626 s.run(&mut x);
627 s.run_dual(&mut x, &mut y);
628 s.run_with_gain(&mut x, &|_, _, _| {});
629 SplitStepF32::new(&m, steps, 0, 64e9).run(&mut []);
630 assert!(x.is_empty() && y.is_empty() && s.steps() == steps);
631 assert!((steps.length_km() - 3.0).abs() < 1e-15);
632 }
633
634 #[test]
635 fn a_uniform_gain_of_the_loss_is_the_attenuation_of_the_medium() {
636 let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
639 let lossless = Medium { alpha_np_km: 0.0, ..m };
640 let x: Vec<C64> = (0..256).map(|n| cis(0.3 * n as f64) * (1e-2 * (1 + n % 5) as f64)).collect();
641 let steps = Steps::new(40, 0.5);
642 let (mut a, mut b) = (x.clone(), x);
643 SplitStep::new(&m, steps, 256, 64e9).run(&mut a);
644 let loss = |z0: f64, z1: f64, out: &mut [f64]| out.fill((-(0.046 / 2.0) * (z1 - z0)).exp());
645 SplitStep::new(&lossless, steps, 256, 64e9).run_with_gain(&mut b, &loss);
646 let peak = a.iter().map(|v| v.norm()).fold(0.0, f64::max);
647 assert!(a.iter().zip(&b).all(|(u, v)| (u - v).norm() < 1e-13 * peak));
648 }
649
650 #[test]
651 fn lossless_split_step_conserves_energy() {
652 let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
653 let mut e: Vec<C64> = (0..256).map(|n| cis(0.1 * n as f64) * (1e-2 * (n % 7) as f64)).collect();
654 let e0: f64 = e.iter().map(C64::norm_sqr).sum();
655 SplitStep::new(&m, Steps::new(80, 1.0), 256, 64e9).run(&mut e);
656 let e1: f64 = e.iter().map(C64::norm_sqr).sum();
657 assert!((e1 / e0 - 1.0).abs() < 1e-12);
658 }
659
660 fn dual_field(n: usize) -> (Vec<C64>, Vec<C64>) {
662 let w = |k: usize, f: f64, ph: f64| (2.0 * std::f64::consts::PI * f * k as f64 / n as f64 + ph).sin();
663 let x = (0..n)
664 .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))))
665 .collect();
666 let y = (0..n)
667 .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))))
668 .collect();
669 (x, y)
670 }
671
672 #[test]
673 fn manakov_with_one_polarisation_is_the_scalar_equation_with_8_9_gamma() {
674 let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
675 let (x0, _) = dual_field(512);
676 let (mut x, mut y) = (x0.clone(), vec![C64::new(0.0, 0.0); 512]);
677 let s = ManakovSplitStep {
678 rule: StepRule::Fixed(0.01),
679 max_iterations: 20,
680 tolerance: 1e-12,
681 ..ManakovSplitStep::new(m)
682 };
683 s.run(&mut x, &mut y, 20.0, 128e9).unwrap();
684 let mut e = x0;
685 let scalar = Medium { gamma_w_km: 8.0 / 9.0 * 1.3, ..m };
686 SplitStep::new(&scalar, Steps::new(2000, 0.01), 512, 128e9).run(&mut e);
687 let err = x.iter().zip(&e).map(|(u, v)| (u - v).norm()).fold(0.0, f64::max);
688 let peak = e.iter().map(|v| v.norm()).fold(0.0, f64::max);
689 assert!(err < 1e-4 * peak, "{}", err / peak);
690 assert!(y.iter().all(|v| v.norm() == 0.0));
691 }
692
693 #[test]
694 fn manakov_rejects_a_step_that_does_not_advance_instead_of_looping() {
695 let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
696 for rule in [
697 StepRule::Fixed(0.0),
698 StepRule::Fixed(-1.0),
699 StepRule::Fixed(f64::NAN),
700 StepRule::NonlinearPhase(0.0),
701 StepRule::NonlinearPhase(-2e-2),
702 ] {
703 let (mut x, mut y) = dual_field(64);
704 let s = ManakovSplitStep { rule, ..ManakovSplitStep::new(m) };
705 assert!(matches!(s.run(&mut x, &mut y, 1.0, 256e9), Err(Error::Config(_))), "{rule:?}");
706 }
707 let s = ManakovSplitStep::new(m);
708 let (mut x, mut y) = dual_field(64);
709 assert!(matches!(s.run(&mut x, &mut y, f64::INFINITY, 256e9), Err(Error::Config(_))));
710 assert!(matches!(s.run(&mut x, &mut y[..32], 1.0, 256e9), Err(Error::Shape(_))));
711 assert_eq!(s.run(&mut x, &mut y, 0.0, 256e9).unwrap(), 0);
712 x[0] = C64::new(f64::INFINITY, 0.0);
714 assert!(matches!(s.run(&mut x, &mut y, 1.0, 256e9), Err(Error::Numerical(_))));
715 }
716
717 #[test]
718 fn manakov_conserves_the_total_energy_and_converges_with_the_step() {
719 let m = Medium { alpha_np_km: 0.0, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
720 let run = |rule| {
721 let (mut x, mut y) = dual_field(1024);
722 let s = ManakovSplitStep { rule, ..ManakovSplitStep::new(m) };
723 let steps = s.run(&mut x, &mut y, 80.0, 256e9).unwrap();
724 (x, y, steps)
725 };
726 let (x0, y0) = dual_field(1024);
727 let e0: f64 = x0.iter().chain(&y0).map(C64::norm_sqr).sum();
728 let (xa, ya, na) = run(StepRule::NonlinearPhase(2e-2));
729 let (xb, yb, nb) = run(StepRule::NonlinearPhase(2e-4));
730 let e1: f64 = xa.iter().chain(&ya).map(C64::norm_sqr).sum();
731 assert!((e1 / e0 - 1.0).abs() < 1e-10);
732 assert!(nb > 50 * na);
733 let err: f64 = xa.iter().zip(&xb).chain(ya.iter().zip(&yb)).map(|(u, v)| (u - v).norm_sqr()).sum();
735 assert!((err / e0).sqrt() < 1e-3, "{}", (err / e0).sqrt());
736 }
737
738 #[test]
739 fn the_fwm_rule_follows_eq_13_of_musetti_and_grows_with_the_loss() {
740 let (n, fs) = (1024, 1.024e12);
742 let tone =
743 |f: f64| -> Vec<C64> { (0..n).map(|k| cis(2.0 * std::f64::consts::PI * f * k as f64 / fs)).collect() };
744 let x: Vec<C64> = tone(-100e9).iter().zip(tone(100e9)).map(|(a, b)| a + b).collect();
745 let y = vec![C64::new(0.0, 0.0); n];
746 assert!((signal_bandwidth(&[&x, &y], fs) - 201e9).abs() < 1.0);
747 assert_eq!(signal_bandwidth(&[&y], fs), 0.0);
748 let m = Medium {
750 alpha_np_km: 0.2 / 10.0 * std::f64::consts::LN_10,
751 beta2_s2_km: -2.1683e-23,
752 gamma_w_km: 1.3,
753 beta3_s3_km: 0.0,
754 };
755 let s = ManakovSplitStep {
756 rule: StepRule::Fwm {
757 phase_rad: 20.0,
758 bandwidth_hz: Some(350e9),
759 update: StepUpdate::ConstantLocalError,
760 max_nl_phase_rad: None,
761 },
762 ..ManakovSplitStep::new(m)
763 };
764 let h1 = s.step_length(&[0.0], 0.0, 100.0, None, 350e9).unwrap();
765 assert!((h1 - 20.0 / (2.1683e-23 * (2.0 * std::f64::consts::PI * 350e9).powi(2))).abs() < 1e-12);
766 assert!((h1 - 0.190).abs() < 0.001, "{h1}");
767 let h2 = s.step_length(&[0.0], h1, 100.0, Some(h1), 350e9).unwrap();
769 assert!((h2 / h1 - (m.alpha_np_km * h1 / 3.0).exp()).abs() < 1e-12);
770 let lossless = ManakovSplitStep { medium: Medium { alpha_np_km: 0.0, ..m }, ..s };
771 assert_eq!(lossless.step_length(&[0.0], h1, 100.0, Some(h1), 350e9).unwrap(), h1);
772 let nlp = ManakovSplitStep {
774 rule: StepRule::Fwm {
775 phase_rad: 20.0,
776 bandwidth_hz: Some(350e9),
777 update: StepUpdate::NonlinearPhase,
778 max_nl_phase_rad: None,
779 },
780 ..s
781 };
782 let h2n = nlp.step_length(&[0.0], h1, 100.0, Some(h1), 350e9).unwrap();
783 let leff = |h: f64| (1.0 - (-m.alpha_np_km * h).exp()) / m.alpha_np_km;
784 assert!((leff(h2n) / leff(h1) - (m.alpha_np_km * h1).exp()).abs() < 1e-12);
785 assert!(h2n > h2);
786 for rule in [
788 StepRule::Fwm {
789 phase_rad: 0.0,
790 bandwidth_hz: None,
791 update: StepUpdate::ConstantLocalError,
792 max_nl_phase_rad: None,
793 },
794 StepRule::Fwm {
795 phase_rad: 20.0,
796 bandwidth_hz: Some(0.0),
797 update: StepUpdate::ConstantLocalError,
798 max_nl_phase_rad: None,
799 },
800 StepRule::Fwm {
801 phase_rad: 20.0,
802 bandwidth_hz: None,
803 update: StepUpdate::ConstantLocalError,
804 max_nl_phase_rad: Some(0.0),
805 },
806 ] {
807 let (mut a, mut b) = (x.clone(), y.clone());
808 assert!(ManakovSplitStep { rule, ..s }.run(&mut a, &mut b, 1.0, fs).is_err());
809 }
810 }
811
812 #[test]
813 fn the_third_order_dispersion_adds_a_cubic_phase() {
814 let m = Medium { beta2_s2_km: -2.17e-23, beta3_s3_km: 1.3e-37, ..Medium::default() };
815 let omega = fft::angular_frequencies(64, 64e9);
816 let r = m.linear_response(&omega, 50.0);
817 for (v, w) in r.iter().zip(&omega) {
818 let phase = (m.beta2_s2_km / 2.0 * w * w + m.beta3_s3_km / 6.0 * w * w * w) * 50.0;
819 assert!((v - cis(phase)).norm() < 1e-12);
820 }
821 let back = m.negated().linear_response(&omega, 50.0);
823 assert!(r.iter().zip(&back).all(|(a, b)| ((a * b) - C64::new(1.0, 0.0)).norm() < 1e-12));
824 }
825
826 #[test]
827 fn one_field_is_the_scalar_split_step_and_fixed_steps_are_equal() {
828 let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
829 let (x0, _) = dual_field(1024);
830 let fs = 256e9;
831 let mut scalar = x0.clone();
833 SplitStep::new(&m, Steps::new(4, 2.5), 1024, fs).run(&mut scalar);
834 let engine = ManakovSplitStep { rule: StepRule::Fixed(3.0), ..ManakovSplitStep::new(m) };
835 let mut x = x0.clone();
836 assert_eq!(engine.run_fields(&mut [&mut x], 10.0, fs, &PropagationOptions::default()).unwrap(), 4);
837 let e: f64 = scalar.iter().map(C64::norm_sqr).sum();
838 let d: f64 = x.iter().zip(&scalar).map(|(u, v)| (u - v).norm_sqr()).sum();
839 assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
840 }
841
842 #[test]
843 fn the_birefringence_of_the_sections_is_their_lumped_response() {
844 let m = Medium { alpha_np_km: 0.0, beta2_s2_km: 0.0, gamma_w_km: 0.0, beta3_s3_km: 0.0 };
846 let (n, fs) = (256, 256e9);
847 let (x0, y0) = dual_field(n);
848 let rot = |t: f64, p: f64| -> [C64; 4] {
849 let (s, c) = t.sin_cos();
850 [C64::new(c, 0.0), -cis(-p) * s, cis(p) * s, C64::new(c, 0.0)]
851 };
852 let rotations = [rot(0.3, 0.1), rot(1.1, -0.7), rot(-0.4, 2.0)];
853 let dgd = [3e-12, 1e-12, 2e-12];
854 let b = Birefringence { section_km: 2.0, rotations: &rotations, dgd_s_per_km: &dgd };
855 let engine = ManakovSplitStep { rule: StepRule::Fixed(0.7), ..ManakovSplitStep::new(m) };
856 let (mut x, mut y) = (x0.clone(), y0.clone());
857 let options = PropagationOptions { gain: None, birefringence: Some(b) };
858 let steps = engine.run_fields(&mut [&mut x, &mut y], 6.0, fs, &options).unwrap();
859 assert_eq!(steps, 9); let omega = fft::angular_frequencies(n, fs);
862 let (fx, fy) = (fft::fft(&x0), fft::fft(&y0));
863 let mut ex = vec![C64::new(0.0, 0.0); n];
864 let mut ey = ex.clone();
865 for k in 0..n {
866 let (mut a, mut c) = (fx[k], fy[k]);
867 for (r, t) in rotations.iter().zip(&dgd) {
868 (a, c) = (r[0] * a + r[1] * c, r[2] * a + r[3] * c);
869 let ph = 0.5 * omega[k] * t * 2.0;
870 (a, c) = (a * cis(-ph), c * cis(ph));
871 }
872 ex[k] = a;
873 ey[k] = c;
874 }
875 let (ex, ey) = (fft::ifft(&ex), fft::ifft(&ey));
876 let e: f64 = ex.iter().chain(&ey).map(C64::norm_sqr).sum();
877 let d: f64 = x.iter().zip(&ex).chain(y.iter().zip(&ey)).map(|(u, v)| (u - v).norm_sqr()).sum();
878 assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
879 let mut z = x0.clone();
881 assert!(engine.run_fields(&mut [&mut z], 6.0, fs, &options).is_err());
882 assert!(engine.run_fields(&mut [&mut x, &mut y], 5.0, fs, &options).is_err());
883 let (mut a, mut b2, mut c) = (x0.clone(), x0.clone(), x0);
884 assert!(engine.run_fields(&mut [&mut a, &mut b2, &mut c], 1.0, fs, &PropagationOptions::default()).is_err());
885 assert!(engine.run_fields(&mut [], 1.0, fs, &PropagationOptions::default()).is_err());
886 }
887
888 #[test]
889 fn the_midpoint_manakov_step_is_the_scalar_split_step() {
890 let m = Medium { alpha_np_km: 0.046, beta2_s2_km: -2.17e-23, gamma_w_km: 1.3, beta3_s3_km: 0.0 };
892 let (x0, _) = dual_field(1024);
893 let (h, n_steps, fs) = (0.5, 160, 256e9);
894 let mut scalar = x0.clone();
895 SplitStep::new(&m, Steps::new(n_steps, h), 1024, fs).run(&mut scalar);
896 let solver = ManakovSplitStep {
897 rule: StepRule::Fixed(h),
898 nonlinear: NonlinearStep::Midpoint,
899 ..ManakovSplitStep::new(Medium { gamma_w_km: 9.0 / 8.0 * m.gamma_w_km, ..m })
900 };
901 let (mut x, mut y) = (x0, vec![C64::new(0.0, 0.0); 1024]);
902 assert_eq!(solver.run(&mut x, &mut y, n_steps as f64 * h, fs).unwrap(), n_steps);
903 let e: f64 = scalar.iter().map(C64::norm_sqr).sum();
904 let d: f64 = x.iter().zip(&scalar).map(|(u, v)| (u - v).norm_sqr()).sum();
905 assert!((d / e).sqrt() < 1e-12, "{}", (d / e).sqrt());
906 assert!(y.iter().all(|v| v.norm() == 0.0));
907 assert_eq!("Midpoint".parse::<NonlinearStep>().unwrap(), NonlinearStep::Midpoint);
908 assert!("euler".parse::<NonlinearStep>().is_err());
909 }
910
911 #[test]
912 fn fundamental_soliton_keeps_its_shape() {
913 let (t0, b2, gamma): (f64, f64, f64) = (10e-12, -20e-24, 1.0);
915 let p0 = b2.abs() / (gamma * t0 * t0);
916 let (n, fs) = (1024, 2e12);
917 let a0: Vec<C64> = (0..n)
918 .map(|k| {
919 let t = (k as f64 - n as f64 / 2.0) / fs;
920 C64::new(p0.sqrt() / (t / t0).cosh(), 0.0)
921 })
922 .collect();
923 let m = Medium { alpha_np_km: 0.0, beta2_s2_km: b2, gamma_w_km: gamma, beta3_s3_km: 0.0 };
924 let mut a = a0.clone();
925 SplitStep::new(&m, Steps::new(1000, 0.01), n, fs).run(&mut a);
927 let err = a.iter().zip(&a0).map(|(u, v)| (u.norm() - v.norm()).abs()).fold(0.0, f64::max);
928 assert!(err < 1e-3 * p0.sqrt(), "relative envelope error {}", err / p0.sqrt());
929 }
930}