Skip to main content

comnum_math/nlse/
manakov.rs

1//! The split-step solver of the Manakov equation, [`ManakovSplitStep`]: two polarisations,
2//! the nonlinear rotation and the birefringence of the fibre.
3
4use super::{FieldGain, Medium, NonlinearStep, StepRule, StepUpdate, manakov_rotation, mul, scale, signal_bandwidth};
5use crate::fft;
6use crate::{C64, cis};
7use comnum_core::{Error, Result, ensure, same_len};
8
9/// The symmetric split-step solver of the Manakov equation (see the module).
10///
11/// With [`NonlinearStep::Trapezoidal`] (OptiCommPy's scheme) the solver is second order
12/// but its error on a collision of two solitons 800 GHz apart is 6.4 times that of
13/// [`NonlinearStep::Midpoint`] (the default) at the same steps (`validation/tests/fiber/sinkin2003.rs`),
14/// and it underestimates the cross-channel interference of a WDM link whose channels
15/// walk off within a step.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct ManakovSplitStep {
18    /// The fibre.
19    pub medium: Medium,
20    /// The step size.
21    pub rule: StepRule,
22    /// The nonlinear step.
23    pub nonlinear: NonlinearStep,
24    /// Largest number of trapezoidal iterations per step.
25    pub max_iterations: usize,
26    /// Convergence tolerance of the iterations: the relative change of the fields.
27    pub tolerance: f64,
28}
29
30/// The birefringence of a span cut into sections of equal length (the PMD of
31/// `comnum_channel::optical::pmd::PmdLink`): section $k$ starts with the frequency-independent
32/// rotation `rotations[k]` (a row-major 2 × 2 matrix applied to $(A_{x}, A_{y})$) and
33/// then delays the two polarisations of its frame by $\mp \tau'_{k} z / 2$, its DGD per
34/// kilometre `dgd_s_per_km[k]` distributed along it. Without nonlinearity a span is the
35/// lumped response $\prod_{k} D_{k}(\omega) R_{k}$, $D_{k}$ the DGD of the whole section.
36#[derive(Clone, Copy, Debug)]
37pub struct Birefringence<'a> {
38    /// The length of a section (km).
39    pub section_km: f64,
40    /// The rotation at the start of every section.
41    pub rotations: &'a [[C64; 4]],
42    /// The DGD of every section per kilometre (s/km).
43    pub dgd_s_per_km: &'a [f64],
44}
45
46/// What a propagation adds to the loss and dispersion of the medium (see
47/// [`ManakovSplitStep::run_fields`]).
48#[derive(Clone, Copy, Default)]
49pub struct PropagationOptions<'a> {
50    /// A distributed gain (see [`SplitStep::run_with_gain`](super::SplitStep::run_with_gain)): the medium then carries no
51    /// attenuation, the gain holds the power profile of the span.
52    pub gain: Option<FieldGain<'a>>,
53    /// The birefringence of the span (two polarisations only).
54    pub birefringence: Option<Birefringence<'a>>,
55}
56
57impl std::fmt::Debug for PropagationOptions<'_> {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct("PropagationOptions")
60            .field("gain", &self.gain.map(|_| "FieldGain"))
61            .field("birefringence", &self.birefringence)
62            .finish()
63    }
64}
65
66/// The buffers of a propagation of [`ManakovSplitStep`], allocated once per run.
67struct Work {
68    /// The angular frequencies of the bins.
69    omega: Vec<f64>,
70    /// $P(z)$, the total power at the start of the step.
71    p0: Vec<f64>,
72    /// The total power of the last iteration.
73    p1: Vec<f64>,
74    /// The phase per kilometre of the trapezoid.
75    phi: Vec<f64>,
76    /// The scalar half linear step.
77    lin: Vec<C64>,
78    /// The two half steps of every field (the DGD of a section, a distributed gain).
79    half1: Vec<Vec<C64>>,
80    half2: Vec<Vec<C64>>,
81    gain: Vec<f64>,
82    /// The fields after the first half step, of the current iteration, and of the last
83    /// iteration (at the start of a step, the field at z).
84    h: Vec<Vec<C64>>,
85    f: Vec<Vec<C64>>,
86    c: Vec<Vec<C64>>,
87}
88
89impl ManakovSplitStep {
90    /// The step rule of Musetti et al. ([`StepRule::FWM_DEFAULT`]: about 0.01 dB of error
91    /// on the SNR whatever the power), the nonlinear phase at the middle of the step; with
92    /// [`NonlinearStep::Trapezoidal`], OptiCommPy's iterations: at most 10, tolerance
93    /// $10^{-5}$. OptiCommPy's own step, 0.02 rad of nonlinear phase, is
94    /// `StepRule::NonlinearPhase(2e-2)`: it depends on the power and is not converged in
95    /// every WDM link (`specs/raman_wdm.yaml`).
96    pub fn new(medium: Medium) -> ManakovSplitStep {
97        ManakovSplitStep {
98            medium,
99            rule: StepRule::FWM_DEFAULT,
100            nonlinear: NonlinearStep::Midpoint,
101            max_iterations: 10,
102            tolerance: 1e-5,
103        }
104    }
105
106    /// Propagate the two polarisations (√W, time domain, equal lengths) over
107    /// `length_km` of fibre sampled at `fs`, in place: the number of steps (none for a
108    /// length that is not positive).
109    ///
110    /// # Errors
111    /// As [`ManakovSplitStep::run_fields`].
112    pub fn run(&self, x: &mut [C64], y: &mut [C64], length_km: f64, fs: f64) -> Result<usize> {
113        self.run_fields(&mut [x, y], length_km, fs, &PropagationOptions::default())
114    }
115
116    /// [`ManakovSplitStep::run`] with a distributed gain (see
117    /// [`SplitStep::run_with_gain`](super::SplitStep::run_with_gain)): the medium carries no attenuation, `gain` holds the
118    /// power profile of the span.
119    ///
120    /// # Errors
121    /// As [`ManakovSplitStep::run_fields`].
122    pub fn run_with_gain(
123        &self,
124        x: &mut [C64],
125        y: &mut [C64],
126        length_km: f64,
127        fs: f64,
128        gain: FieldGain<'_>,
129    ) -> Result<usize> {
130        self.run_fields(&mut [x, y], length_km, fs, &PropagationOptions { gain: Some(gain), birefringence: None })
131    }
132
133    /// Propagate one field (the scalar NLSE, nonlinear coefficient $\gamma$) or two (the
134    /// Manakov equation, $\frac{8}{9}\gamma$) over `length_km` sampled at `fs`, in place,
135    /// with the additions of `options`: the engine of every forward propagation
136    /// (`docs/adr/0014-one-fibre-propagator.md`). The steps follow [`ManakovSplitStep::rule`]
137    /// and are cut at the boundaries of the sections of a birefringence; a fixed step
138    /// covers every stretch with equal steps of at most that length. Returns the number
139    /// of steps.
140    ///
141    /// # Errors
142    /// [`Error::Shape`] for no field, more than two, fields of different lengths, a
143    /// birefringence with one field or sections that do not cover the length;
144    /// [`Error::Config`] for a fixed step, a rotation or a phase that is not positive or
145    /// a length that is not finite; [`Error::Numerical`] if a step vanishes (a non-finite
146    /// field).
147    pub fn run_fields(
148        &self,
149        fields: &mut [&mut [C64]],
150        length_km: f64,
151        fs: f64,
152        options: &PropagationOptions<'_>,
153    ) -> Result<usize> {
154        let nf = fields.len();
155        if nf == 0 || nf > 2 {
156            return Err(Error::Shape(format!("a propagation takes one or two fields, got {nf}")));
157        }
158        let n = fields[0].len();
159        if nf == 2 {
160            same_len(n, fields[1].len(), "the polarisations")?;
161        }
162        self.rule_parameter()?;
163        ensure(length_km.is_finite(), || format!("the length must be finite, got {length_km} km"))?;
164        if let Some(b) = &options.birefringence {
165            check_birefringence(b, nf, length_km)?;
166        }
167        if n == 0 || length_km <= 0.0 {
168            return Ok(0);
169        }
170        if let (StepRule::Fixed(hf), NonlinearStep::Midpoint) = (self.rule, self.nonlinear) {
171            return Ok(self.run_fixed_midpoint(fields, length_km, fs, options, hf));
172        }
173        let p = fft::plan(n);
174        let zero = C64::new(0.0, 0.0);
175        let mut w = Work {
176            omega: fft::angular_frequencies(n, fs),
177            p0: vec![0.0; n],
178            p1: vec![0.0; n],
179            phi: vec![0.0; n],
180            lin: vec![zero; n],
181            half1: vec![vec![zero; n]; nf],
182            half2: vec![vec![zero; n]; nf],
183            gain: vec![0.0; n],
184            h: vec![vec![zero; n]; nf],
185            f: vec![vec![zero; n]; nf],
186            c: fields.iter().map(|u| u.to_vec()).collect(),
187        };
188        let k = if nf == 2 { 8.0 / 9.0 } else { 1.0 } * self.medium.gamma_w_km;
189        let bandwidth = match self.rule {
190            StepRule::Fwm { bandwidth_hz: Some(b), .. } => b,
191            StepRule::Fwm { bandwidth_hz: None, .. } => {
192                let views: Vec<&[C64]> = fields.iter().map(|u| &**u).collect();
193                signal_bandwidth(&views, fs)
194            }
195            _ => 0.0,
196        };
197        let mut z = 0.0;
198        let mut steps = 0;
199        let mut previous = None;
200        // the section of the birefringence the steps are in, and the next boundary
201        let mut section: Option<usize> = None;
202        while z < length_km {
203            let mut stretch_end = length_km;
204            if let Some(b) = &options.birefringence {
205                let current = ((z / b.section_km) * (1.0 + 1e-12)).floor() as usize;
206                let current = current.min(b.rotations.len() - 1);
207                if section != Some(current) {
208                    // the rotation at the start of the section, on the fields and on the
209                    // field of the last iteration (a unitary, local change of frame)
210                    let (a, rest) = fields.split_at_mut(1);
211                    rotate(&b.rotations[current], a[0], rest[0]);
212                    let (a, rest) = w.c.split_at_mut(1);
213                    rotate(&b.rotations[current], &mut a[0], &mut rest[0]);
214                    section = Some(current);
215                }
216                stretch_end = (((current + 1) as f64) * b.section_km).min(length_km);
217            }
218            total_power_of(fields, &mut w.p0);
219            total_power_of(&w.c, &mut w.p1);
220            trapezoid(&w.p0, &w.p1, k, &mut w.phi);
221            let h = self.step_length(&w.phi, z, stretch_end - z, previous, bandwidth)?;
222            previous = Some(h);
223            self.half_steps(&mut w, z, h, section, options);
224            for (o, u) in w.h.iter_mut().zip(fields.iter()) {
225                o.copy_from_slice(u);
226            }
227            for (u, half) in w.h.iter_mut().zip(&w.half1) {
228                p.forward(u);
229                mul(u, half);
230                p.inverse(u);
231            }
232            if self.nonlinear == NonlinearStep::Midpoint {
233                // the phase of the power after the first half of the dispersion
234                kerr_rotation(&mut w.h, k * h);
235                for (u, half) in w.h.iter_mut().zip(&w.half2) {
236                    p.forward(u);
237                    mul(u, half);
238                    p.inverse(u);
239                }
240                for ((u, c), hh) in fields.iter_mut().zip(w.c.iter_mut()).zip(&w.h) {
241                    u.copy_from_slice(hh);
242                    c.copy_from_slice(hh);
243                }
244            } else {
245                for it in 0..self.max_iterations.max(1) {
246                    for (o, v) in w.f.iter_mut().zip(&w.h) {
247                        for ((a, b), ph) in o.iter_mut().zip(v).zip(&w.phi) {
248                            *a = b * cis(ph * h);
249                        }
250                    }
251                    for (u, half) in w.f.iter_mut().zip(&w.half2) {
252                        p.forward(u);
253                        mul(u, half);
254                        p.inverse(u);
255                    }
256                    let diff: f64 =
257                        w.f.iter().zip(&w.c).flat_map(|(a, b)| a.iter().zip(b)).map(|(u, v)| (u - v).norm_sqr()).sum();
258                    let norm: f64 = w.c.iter().flatten().map(C64::norm_sqr).sum();
259                    std::mem::swap(&mut w.c, &mut w.f);
260                    if norm == 0.0 || (diff / norm).sqrt() < self.tolerance || it + 1 == self.max_iterations {
261                        break;
262                    }
263                    total_power_of(&w.c, &mut w.p1);
264                    trapezoid(&w.p0, &w.p1, k, &mut w.phi);
265                }
266                for (u, c) in fields.iter_mut().zip(&w.c) {
267                    u.copy_from_slice(c);
268                }
269            }
270            z += h;
271            steps += 1;
272        }
273        Ok(steps)
274    }
275
276    /// [`ManakovSplitStep::run_fields`] with fixed steps of at most `hf` and the phase at
277    /// the middle of the step: the fields stay in the frequency domain from the second half
278    /// step of a step to the first of the next (two FFTs per step and field, as
279    /// [`SplitStep`]), and come back to the time domain only for the nonlinear phase and
280    /// at the boundaries of the sections.
281    fn run_fixed_midpoint(
282        &self,
283        fields: &mut [&mut [C64]],
284        length_km: f64,
285        fs: f64,
286        options: &PropagationOptions<'_>,
287        hf: f64,
288    ) -> usize {
289        let (nf, n) = (fields.len(), fields[0].len());
290        let p = fft::plan(n);
291        let zero = C64::new(0.0, 0.0);
292        let mut w = Work {
293            omega: fft::angular_frequencies(n, fs),
294            p0: Vec::new(),
295            p1: Vec::new(),
296            phi: Vec::new(),
297            lin: vec![zero; n],
298            half1: vec![vec![zero; n]; nf],
299            half2: vec![vec![zero; n]; nf],
300            gain: vec![0.0; n],
301            h: fields.iter().map(|u| u.to_vec()).collect(),
302            f: Vec::new(),
303            c: Vec::new(),
304        };
305        let k = if nf == 2 { 8.0 / 9.0 } else { 1.0 } * self.medium.gamma_w_km;
306        // the stretches: the sections of the birefringence, or the whole length
307        let (n_stretches, stretch_km) = match &options.birefringence {
308            Some(b) => (b.rotations.len(), b.section_km),
309            None => (1, length_km),
310        };
311        for u in &mut w.h {
312            p.forward(u);
313        }
314        let mut steps = 0;
315        for s in 0..n_stretches {
316            let z_start = s as f64 * stretch_km;
317            let z_end = if s + 1 == n_stretches { length_km } else { (s + 1) as f64 * stretch_km };
318            if let Some(b) = &options.birefringence {
319                // the rotation at the start of the section, in the frequency domain (it does
320                // not depend on the frequency)
321                let (a, rest) = w.h.split_at_mut(1);
322                rotate(&b.rotations[s], &mut a[0], &mut rest[0]);
323            }
324            let (n_steps, h) = equal_steps(z_end - z_start, hf);
325            let section = options.birefringence.is_some().then_some(s);
326            // the half steps without the gain, which multiplies after them
327            self.half_steps(&mut w, z_start, h, section, &PropagationOptions { gain: None, ..*options });
328            for i in 0..n_steps {
329                let z = z_start + i as f64 * h;
330                for (u, half) in w.h.iter_mut().zip(&w.half1) {
331                    mul(u, half);
332                }
333                if let Some(g) = options.gain {
334                    g(z, z + h / 2.0, &mut w.gain);
335                    for u in &mut w.h {
336                        scale(u, &w.gain);
337                    }
338                }
339                for u in &mut w.h {
340                    p.inverse(u);
341                }
342                kerr_rotation_scalar_order(&mut w.h, k, h);
343                for (u, half) in w.h.iter_mut().zip(&w.half2) {
344                    p.forward(u);
345                    mul(u, half);
346                }
347                if let Some(g) = options.gain {
348                    g(z + h / 2.0, z + h, &mut w.gain);
349                    for u in &mut w.h {
350                        scale(u, &w.gain);
351                    }
352                }
353                steps += 1;
354            }
355        }
356        for (u, spectrum) in fields.iter_mut().zip(w.h.iter_mut()) {
357            p.inverse(spectrum);
358            u.copy_from_slice(spectrum);
359        }
360        steps
361    }
362
363    /// The two half linear steps of every field for the step from `z` of length `h`: the
364    /// loss and dispersion of the medium, the DGD of the section, the distributed gain.
365    fn half_steps(&self, w: &mut Work, z: f64, h: f64, section: Option<usize>, options: &PropagationOptions<'_>) {
366        self.medium.half_step_into(&w.omega, h, &mut w.lin);
367        let nf = w.half1.len();
368        let dgd = match (options.birefringence, section) {
369            (Some(b), Some(s)) => b.dgd_s_per_km[s],
370            _ => 0.0,
371        };
372        for i in 0..nf {
373            // the frame delays x by +τ'h/4 and y by -τ'h/4 over a half step
374            let sign = if i == 0 { -1.0 } else { 1.0 };
375            for ((a, b), (l, wk)) in w.half1[i].iter_mut().zip(w.half2[i].iter_mut()).zip(w.lin.iter().zip(&w.omega)) {
376                let d = if dgd == 0.0 { *l } else { l * cis(sign * 0.25 * wk * dgd * h) };
377                *a = d;
378                *b = d;
379            }
380        }
381        if let Some(g) = options.gain {
382            for (half, z0, z1) in [(&mut w.half1, z, z + h / 2.0), (&mut w.half2, z + h / 2.0, z + h)] {
383                g(z0, z1, &mut w.gain);
384                for v in half.iter_mut() {
385                    for (o, b) in v.iter_mut().zip(&w.gain) {
386                        *o *= b;
387                    }
388                }
389            }
390        }
391    }
392
393    /// The parameter of the step rule, checked: the fixed step (km), the rotation or the
394    /// phase of the four-wave mixing (rad).
395    fn rule_parameter(&self) -> Result<f64> {
396        let (step, what) = match self.rule {
397            StepRule::Fixed(h) => (h, "fixed step (km)"),
398            StepRule::NonlinearPhase(r) => (r, "nonlinear rotation (rad)"),
399            StepRule::Fwm { phase_rad, bandwidth_hz, max_nl_phase_rad, .. } => {
400                if let Some(b) = bandwidth_hz {
401                    ensure(b > 0.0 && b.is_finite(), || format!("the bandwidth must be positive, got {b} Hz"))?;
402                }
403                if let Some(r) = max_nl_phase_rad {
404                    ensure(r > 0.0, || format!("the nonlinear rotation must be positive, got {r}"))?;
405                }
406                (phase_rad, "four-wave-mixing phase (rad)")
407            }
408        };
409        ensure(step > 0.0, || format!("the {what} must be positive, got {step}"))?;
410        Ok(step)
411    }
412
413    /// The first step (km) of [`StepRule::Fwm`] for the bandwidth `b` (Hz), eq. (13) of
414    /// Musetti et al. (the whole length without dispersion or bandwidth).
415    fn fwm_first_step(&self, phase_rad: f64, b: f64) -> f64 {
416        let w = 2.0 * std::f64::consts::PI * b;
417        let h = phase_rad / (self.medium.beta2_s2_km.abs() * w * w);
418        if h.is_finite() { h } else { f64::INFINITY }
419    }
420
421    /// The length of the next step at `z` with `rest` km left in the stretch and the
422    /// phase `phi` (rad/km) of the step. A fixed step `h` covers the rest of the stretch
423    /// with $\lceil \text{rest} / h \rceil$ equal steps.
424    ///
425    /// # Errors
426    /// [`Error::Numerical`] for a step that does not advance `z`.
427    pub(super) fn step_length(
428        &self,
429        phi: &[f64],
430        z: f64,
431        rest: f64,
432        previous: Option<f64>,
433        bandwidth: f64,
434    ) -> Result<f64> {
435        let h = match self.rule {
436            StepRule::Fixed(hf) => equal_steps(rest, hf).1,
437            StepRule::NonlinearPhase(rot) => {
438                let m = phi.iter().copied().fold(0.0, f64::max);
439                if m > 0.0 && rest >= rot / m { rot / m } else { rest }
440            }
441            StepRule::Fwm { phase_rad, update, max_nl_phase_rad, .. } => {
442                let a = self.medium.alpha_np_km;
443                let h = match previous {
444                    None => self.fwm_first_step(phase_rad, bandwidth),
445                    Some(hk) if a <= 0.0 => hk,
446                    Some(hk) => match update {
447                        StepUpdate::ConstantLocalError => hk * (a * hk / 3.0).exp(),
448                        StepUpdate::NonlinearPhase => {
449                            // L_eff(h') = L_eff(h) e^{α h}, h' = -ln(1 - α L_eff(h')) / α
450                            let l = (1.0 - (-a * hk).exp()) / a * (a * hk).exp();
451                            if a * l < 1.0 { -(1.0 - a * l).ln() / a } else { f64::INFINITY }
452                        }
453                    },
454                };
455                let m = phi.iter().copied().fold(0.0, f64::max);
456                let nl = match max_nl_phase_rad {
457                    Some(rot) if m > 0.0 => rot / m,
458                    _ => f64::INFINITY,
459                };
460                h.min(nl).min(rest)
461            }
462        };
463        // a step that does not advance (an infinite power, a step below the precision
464        // of z) would never reach the end of the fibre
465        if z + h <= z || h.is_nan() {
466            return Err(Error::Numerical(format!("the split step vanishes at z = {z} km (step {h} km)")));
467        }
468        Ok(h)
469    }
470}
471
472/// The steps that cover `rest` km with steps of at most `hf`: their number and length,
473/// `hf` itself when it divides `rest` (within $10^{-9}$), `rest / n` otherwise (the steps
474/// of `comnum_channel::optical::fiber::Ssfm`).
475fn equal_steps(rest: f64, hf: f64) -> (usize, f64) {
476    let ratio = rest / hf;
477    let whole = ratio.round();
478    if whole >= 1.0 && (whole - ratio).abs() <= 1e-9 * ratio {
479        (whole as usize, hf)
480    } else {
481        let n = ratio.ceil().max(1.0);
482        (n as usize, rest / n)
483    }
484}
485
486/// [`Error::Shape`] unless a birefringence applies to two fields and its sections cover
487/// `length_km`.
488fn check_birefringence(b: &Birefringence<'_>, nf: usize, length_km: f64) -> Result<()> {
489    if nf != 2 {
490        return Err(Error::Shape("a birefringence needs two polarisations".into()));
491    }
492    let n = b.rotations.len();
493    let covers = b.section_km > 0.0 && ((n as f64) * b.section_km - length_km).abs() <= 1e-9 * length_km.max(1.0);
494    if n == 0 || b.dgd_s_per_km.len() != n || !covers {
495        return Err(Error::Shape(format!(
496            "{n} sections of {} km and {} DGDs for a length of {length_km} km",
497            b.section_km,
498            b.dgd_s_per_km.len()
499        )));
500    }
501    Ok(())
502}
503
504/// The rotation `m` (row-major) applied to $(A_{x}, A_{y})$ at every sample.
505fn rotate(m: &[C64; 4], x: &mut [C64], y: &mut [C64]) {
506    for (u, v) in x.iter_mut().zip(y.iter_mut()) {
507        let (a, b) = (m[0] * *u + m[1] * *v, m[2] * *u + m[3] * *v);
508        *u = a;
509        *v = b;
510    }
511}
512
513/// The total power $\sum_{i} |A_{i}|^{2}$ of the fields at every sample.
514fn total_power_of<T: AsRef<[C64]>>(fields: &[T], out: &mut [f64]) {
515    out.fill(0.0);
516    for f in fields {
517        for (o, v) in out.iter_mut().zip(f.as_ref()) {
518            *o += v.norm_sqr();
519        }
520    }
521}
522
523/// [`kerr_rotation`] of the phase $k |A|^{2} h$ computed in the order of [`SplitStep`] for
524/// one field ($k |A|^{2}$ times $h$), and of [`manakov_rotation`] ($k h$ first) for two:
525/// the fixed steps give the fields of the former solvers bit for bit.
526fn kerr_rotation_scalar_order(fields: &mut [Vec<C64>], k: f64, h: f64) {
527    match fields {
528        [x] => {
529            for v in x.iter_mut() {
530                *v *= cis(k * v.norm_sqr() * h);
531            }
532        }
533        [x, y] => manakov_rotation(x, y, k * h),
534        _ => {}
535    }
536}
537
538/// The Kerr rotation $A_{i} \leftarrow A_{i} e^{j k \sum_{i} |A_{i}|^{2}}$ of one field or
539/// two ([`manakov_rotation`]).
540fn kerr_rotation(fields: &mut [Vec<C64>], k: f64) {
541    match fields {
542        [x] => {
543            for v in x.iter_mut() {
544                *v *= cis(k * v.norm_sqr());
545            }
546        }
547        [x, y] => manakov_rotation(x, y, k),
548        _ => {}
549    }
550}
551
552/// The phase per kilometre of the trapezoidal rule, $\phi = k (P_{0} + P_{1}) / 2$.
553fn trapezoid(p0: &[f64], p1: &[f64], k: f64, phi: &mut [f64]) {
554    for (f, (u, v)) in phi.iter_mut().zip(p0.iter().zip(p1)) {
555        *f = k * (u + v) / 2.0;
556    }
557}