Skip to main content

comnum_spec/blocks/
optical.rs

1//! Optical blocks: wavelength-division multiplexing and dual-polarisation nonlinear
2//! propagation.
3//!
4//! | Block | Does |
5//! |---|---|
6//! | `wdm_tx` | the channel under test and `n_channels - 1` independently modulated neighbours on a frequency grid, shaped, at a power per channel, with laser phase noise |
7//! | `manakov` | dual-polarisation propagation over the multi-span link by the Manakov equation (OptiCommPy's `manakovSSF`), EDFAs with ASE |
8//! | `pmd` | a random rotation of the polarisations and polarisation-mode dispersion (coarse-step model of Marcuse, Menyuk & Wai), a new realisation every trial |
9//! | `manakov_dbp` | dual-polarisation digital back-propagation of the link (the Manakov equation with the coefficients negated) |
10//! | `wdm_rx` | the channel under test brought to baseband and low-pass filtered (an ideal coherent receiver with its local oscillator on the channel) |
11//!
12//! # Dual polarisation
13//!
14//! `source` with `pols: 2` draws the symbols of both polarisations: `symbols` holds those
15//! of polarisation x then those of y, `samples` carries x and the first entry of
16//! `antennas` carries y. The blocks written for one polarisation (pulse shaping,
17//! dispersion compensation, matched filter, clock recovery, frequency and phase
18//! estimation, frame synchronisation) then process each polarisation with its own
19//! symbols; `adaptive_equalizer` is the 2 × 2 butterfly that undoes the rotations of
20//! the polarisation; the metrics pool both polarisations.
21//!
22//! # Example
23//!
24//! ```yaml
25//! fiber: {n_spans: 10, span_km: 80, d_ps_nm_km: 16, gamma_w_km: 1.3}
26//! link:
27//!   - {type: source, constellation: 16-QAM, n_symbols: 8192, symbol_rate: 32e9, pols: 2}
28//!   - {type: wdm_tx, n_channels: 5, spacing_hz: 50e9, sps: 8, power_dbm: $power_dbm, rolloff: 0.1}
29//!   - manakov
30//!   - {type: wdm_rx, sps: 2}
31//!   - cdc
32//!   - {type: matched_filter, rolloff: 0.1}
33//!   - decimate
34//!   - {type: frame_sync, length: 512}
35//! metrics: {report: [ber, snr_eff_db, gmi], skip_start: 64, skip_end: 64}
36//! ```
37//!
38//! # Raman amplification
39//!
40//! A `raman` mapping in the fibre parameters (of the specification or of a block) pumps
41//! every span of `ssfm`, `manakov` and of the theory `gn_snr_db` (see
42//! `comnum_channel::optical::raman`):
43//!
44//! ```yaml
45//! fiber:
46//!   n_spans: 3
47//!   span_km: 100
48//!   raman:
49//!     pumps:                       # any number, co- or counter-propagating
50//!       - {wavelength_nm: 1425, power_mw: 300, direction: counter}
51//!       - {frequency_thz: 205.0, power_mw: 100, direction: co, alpha_db_km: 0.25}
52//!     isrs: true                   # the channels also exchange power (default)
53//!     aeff_um2: 80                 # effective area (default)
54//!     temperature_k: 300           # for the spontaneous noise (default)
55//!     step_km: 0.1                 # grid of the power equations (default)
56//!     slice_hz: 25e9               # slices of the spectrum of the field (default)
57//!     # gain: {offsets_thz: [...], gain_m_w: [...], reference_thz: 206.18}
58//! ```
59//!
60//! The amplifier after every span (`amplification`) then restores every frequency to
61//! its launch power. `dbp` and `manakov_dbp` back-propagate the profile of lumped
62//! amplifiers.
63//!
64//! # References
65//!
66//! 1. E. P. da Silva and D. A. A. Mello, OptiCommPy, `simpleWDMTx` and `manakovSSF`.
67//! 2. P. Poggiolini, "The GN model of non-linear propagation in uncoherent optical
68//!    systems," *J. Lightw. Technol.*, 30(24), 3857–3879, 2012 (the reference of the
69//!    WDM campaigns, see `comnum_channel::optical::gn`).
70
71use super::dsp::PulseSettings;
72use super::fiber::AmplifierSettings;
73use super::fiber::fiber_json;
74use super::{Block, BuildContext, Extension, FIBER_DOC, FiberSpec, Frame, Mapping, NumberOrList};
75use comnum_channel::optical::manakov::ManakovSsfm;
76use comnum_channel::optical::pmd::{Jones, PmdLink};
77use comnum_core::rng::Rng;
78use comnum_core::signal::DualPol;
79use comnum_core::{Error, Result, ensure};
80use comnum_dsp::signal::{wdm_demultiplex, wdm_grid};
81use comnum_math::nlse::{StepRule, StepUpdate};
82use comnum_math::{C64, dbm2w};
83use comnum_systems::wdm::WdmTransmitter;
84use serde_json::Value as Json;
85
86/// The WDM frame between `wdm_tx` and `wdm_rx`.
87#[derive(Clone, Debug)]
88pub struct WdmFrame {
89    /// The centre frequencies of the channels relative to the carrier (Hz).
90    pub freqs_hz: Vec<f64>,
91    /// The index of the channel under test.
92    pub cut: usize,
93}
94
95/// The blocks of this module: their documentation and their builders.
96pub(super) fn blocks() -> Vec<Extension> {
97    vec![
98        builtin!(
99            "wdm_tx",
100            "WDM transmitter: the channel under test and independently modulated neighbours on the grid of OptiCommPy's simpleWDMTx.",
101            WdmTxSpec,
102            needs[Samples],
103            gives[Wdm],
104            |q, _, _| Ok(Box::new(WdmTx::new(&q)?)),
105        ),
106        builtin!(
107            "manakov",
108            "Dual-polarisation nonlinear propagation over the link (Manakov equation, split step with the phase at the middle of the step or OptiCommPy's trapezoidal one), EDFAs with ASE.",
109            ManakovSpec,
110            needs [Samples],
111            gives [],
112            |q, label, ctx| Ok(Box::new(ManakovBlock::new(&q, ctx, label)?)),
113        ),
114        builtin!(
115            "pmd",
116            "Random rotation of the polarisations and polarisation-mode dispersion of a dual-polarisation signal, a new realisation every trial.",
117            PmdBlock,
118            needs [Samples],
119            gives [],
120            |q, label, _| Ok(Box::new(Pmd { p: q, label })),
121        ),
122        builtin!(
123            "manakov_dbp",
124            "Dual-polarisation digital back-propagation of the link (Manakov equation, coefficients negated).",
125            DbpSpec,
126            needs [Samples],
127            gives [],
128            |q, _, ctx| Ok(Box::new(ManakovDbpBlock::new(&q, ctx)?)),
129        ),
130        builtin!(
131            "wdm_rx",
132            "Bring the channel under test to baseband and keep its band (ideal low-pass by resampling).",
133            WdmRx,
134            needs [Samples, Wdm],
135            gives [],
136            |q, _, _| Ok(Box::new(q)),
137        ),
138    ]
139}
140
141// ------------------------------------------------------------------------- wdm_tx
142
143block_params! {
144    struct WdmTxSpec {
145        n_channels: usize [5] >= 1, "channels N; the grid is (k - ⌊N/2⌋) Δf, shifted by Δf/2 when N is even";
146        spacing_hz: f64 [50e9] "Hz" > 0, "grid spacing Δf";
147        sps: usize [8] >= 1, "samples per symbol of the multiplex (the sampling rate must cover the band)";
148        power_dbm: NumberOrList [0.0] "dBm", "launch power per channel, both polarisations; a list gives one per channel";
149        pulse: PulseSettings [flatten], "the pulse of every channel";
150        linewidth_hz: f64 [0.0] "Hz" >= 0, "linewidth of the transmit lasers";
151        channel: Option<usize> [absent "centre"], "index of the channel under test (⌊N/2⌋ by default)";
152        mzm_scale: Option<f64> [absent],
153            "drive the IQ modulator of OptiCommPy at this peak amplitude V/V_π (none: a linear modulator)";
154    }
155}
156
157struct WdmTx {
158    tx: WdmTransmitter,
159    cut: usize,
160}
161
162impl WdmTx {
163    fn new(p: &WdmTxSpec) -> Result<WdmTx> {
164        let powers_dbm: Vec<f64> = match &p.power_dbm.0 {
165            Json::Array(v) => v
166                .iter()
167                .map(|x| x.as_f64().ok_or_else(|| Error::Config("wdm_tx: power_dbm must be numbers".into())))
168                .collect::<Result<_>>()?,
169            v => vec![
170                v.as_f64().ok_or_else(|| Error::Config("wdm_tx: power_dbm must be a number".into()))?;
171                p.n_channels
172            ],
173        };
174        ensure(powers_dbm.len() == p.n_channels, || {
175            format!("wdm_tx: {} powers for {} channels", powers_dbm.len(), p.n_channels)
176        })?;
177        let cut = p.channel.unwrap_or(p.n_channels / 2);
178        ensure(cut < p.n_channels, || format!("wdm_tx: channel {cut} out of the {} channels", p.n_channels))?;
179        let tx = WdmTransmitter {
180            freqs_hz: wdm_grid(p.n_channels, p.spacing_hz),
181            powers_w: powers_dbm.into_iter().map(dbm2w).collect(),
182            taps: p.pulse.taps(p.sps)?,
183            sps: p.sps,
184            linewidth_hz: p.linewidth_hz,
185            mzm_scale: p.mzm_scale,
186        };
187        Ok(WdmTx { tx, cut })
188    }
189}
190
191impl Block for WdmTx {
192    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
193        s.require_one_sps("wdm_tx")?;
194        // the neighbours carry the format of the channel under test (Gaussian symbols
195        // after a gaussian source)
196        let c = s.constellation.clone();
197        let pols = s.n_streams();
198        let n_sym = s.samples.len();
199        let cut_streams = s.take_streams();
200        let out = self.tx.multiplex(pols, n_sym, s.symbol_rate, rng, |ch, pol, rng| {
201            if ch == self.cut {
202                cut_streams[pol].clone()
203            } else {
204                match &c {
205                    Some(c) => (0..n_sym).map(|_| c.points()[rng.below(c.order())]).collect(),
206                    None => (0..n_sym).map(|_| rng.complex_gaussian()).collect(),
207                }
208            }
209        })?;
210        s.set_streams(out);
211        s.sps = self.tx.sps as f64;
212        s.in_flight.wdm = Some(WdmFrame { freqs_hz: self.tx.freqs_hz.clone(), cut: self.cut });
213        Ok(())
214    }
215}
216
217// ------------------------------------------------------------------------ manakov
218
219block_params! {
220    struct ManakovSpec {
221        fiber: Option<Mapping<FiberSpec>> [absent "spec fiber"], FIBER_DOC;
222        step_rule: String ["fwm"] one_of ["fwm", "nonlinear_phase", "logarithmic"],
223            "the steps: the four-wave-mixing phase of Musetti et al. 2018 (power independent), OptiCommPy's nonlinear phase per step, or steps_per_span logarithmic steps (Bosco et al. 2000)";
224        steps_per_span: usize [100] >= 1, "logarithmic steps per span (step_rule logarithmic)";
225        fwm_phase_rad: f64 [20.0] "rad" > 0, "four-wave-mixing phase across the bandwidth in the first step of a span (step_rule fwm)";
226        bandwidth_hz: Option<f64> [absent] "Hz" > 0, "signal bandwidth of the rule fwm (measured on the launched field if absent)";
227        max_nl_phase_rad: f64 [2e-2] "rad" > 0, "largest nonlinear phase rotation per step (step_rule nonlinear_phase)";
228        step_km: Option<f64> [absent] "km" > 0, "a fixed step instead of the adaptive one";
229        nonlinear_step: String ["midpoint"] one_of ["midpoint", "trapezoidal"],
230            "the nonlinear phase of a step: the power at its middle (the scalar solver) or the trapezoidal mean of the power at its ends (OptiCommPy, iterated; less accurate in WDM)";
231        check_sampling: bool [true], "refuse a signal whose sampling rate or block cannot hold the four-wave mixing and the walk-off of the link (Musetti et al. 2018, Appendix B)";
232        max_iterations: usize [10] >= 1, "trapezoidal iterations per step at most";
233        tolerance: f64 [1e-5] > 0, "convergence tolerance of the iterations";
234        amplifiers: AmplifierSettings [flatten], "the amplifiers after every span";
235    }
236}
237
238struct ManakovBlock {
239    link: ManakovSsfm,
240    noise: bool,
241    label: String,
242}
243
244impl ManakovBlock {
245    fn new(p: &ManakovSpec, ctx: &BuildContext, label: String) -> Result<ManakovBlock> {
246        let fiber = ctx.fiber(fiber_json(p.fiber.as_ref()))?;
247        let rule = match (p.step_km, p.step_rule.as_str()) {
248            (Some(h), _) => StepRule::Fixed(h),
249            (None, "nonlinear_phase") => StepRule::NonlinearPhase(p.max_nl_phase_rad),
250            (None, "logarithmic") => StepRule::Logarithmic(p.steps_per_span),
251            (None, _) => StepRule::Fwm {
252                phase_rad: p.fwm_phase_rad,
253                bandwidth_hz: p.bandwidth_hz,
254                update: StepUpdate::ConstantLocalError,
255                max_nl_phase_rad: Some(p.max_nl_phase_rad),
256            },
257        };
258        Ok(ManakovBlock {
259            link: ManakovSsfm {
260                fiber,
261                rule,
262                nonlinear: p.nonlinear_step.parse()?,
263                max_iterations: p.max_iterations,
264                tolerance: p.tolerance,
265                amplification: p.amplifiers.amplification()?,
266                raman: ctx.raman(fiber_json(p.fiber.as_ref()))?,
267                check_sampling: p.check_sampling,
268            },
269            noise: p.amplifiers.noise(),
270            label,
271        })
272    }
273}
274
275impl Block for ManakovBlock {
276    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
277        ensure(s.n_streams() <= 2, || "manakov propagates one or two polarisations".into())?;
278        let fs = s.fs();
279        let mut streams = s.take_streams();
280        let single = streams.len() == 1;
281        if single {
282            streams.push(vec![C64::new(0.0, 0.0); streams[0].len()]);
283        }
284        let y = streams.pop().unwrap_or_default();
285        let x = streams.pop().unwrap_or_default();
286        let mut u = DualPol::new(x, y)?;
287        let steps = self.link.propagate(&mut u, fs, if self.noise { Some(rng) } else { None })?;
288        s.record(&self.label, "steps", steps as f64);
289        s.set_streams(if single { vec![u.x] } else { vec![u.x, u.y] });
290        Ok(())
291    }
292}
293
294// ----------------------------------------------------------------------------- pmd
295
296block_params! {
297    struct PmdBlock {
298        mean_dgd_ps: f64 [0.0] "ps" >= 0, "mean differential group delay of the link; 0 for a rotation only";
299        sections: usize [20] >= 1, "birefringent sections of the coarse-step model";
300        theta_deg: Option<f64> [absent] "deg", "a fixed real rotation by this angle instead, without DGD";
301    }
302}
303
304/// The `pmd` block: its parameters and the prefix of its recorded values.
305struct Pmd {
306    p: PmdBlock,
307    label: String,
308}
309
310impl Block for Pmd {
311    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
312        let PmdBlock { mean_dgd_ps, sections, theta_deg } = self.p;
313        ensure(s.n_streams() == 2, || "pmd needs two polarisations (source with pols: 2)".into())?;
314        let (x, rest) = (&mut s.samples, &mut s.antennas);
315        if let Some(t) = theta_deg {
316            let j = Jones::rotation(t.to_radians());
317            for (u, v) in x.iter_mut().zip(rest[0].iter_mut()) {
318                (*u, *v) = j.apply(*u, *v);
319            }
320            return Ok(());
321        }
322        // the coarse-step model: a rotation only without DGD
323        let n = if mean_dgd_ps == 0.0 { 1 } else { sections };
324        let link = PmdLink::with_mean_dgd(mean_dgd_ps * 1e-12, n, rng);
325        let fs = s.fs();
326        let mut u = DualPol::new(std::mem::take(&mut s.samples), std::mem::take(&mut s.antennas[0]))?;
327        link.apply(&mut u, fs)?;
328        (s.samples, s.antennas[0]) = (u.x, u.y);
329        s.record(&self.label, "rms_dgd_ps", link.rms_dgd_s() * 1e12);
330        Ok(())
331    }
332}
333
334// ---------------------------------------------------------------------- manakov_dbp
335
336block_params! {
337    struct DbpSpec {
338        fiber: Option<Mapping<FiberSpec>> [absent "spec fiber"], FIBER_DOC;
339        steps_per_span: usize [1] >= 1, "symmetric steps per span";
340        nonlinear_scale: f64 [1.0], "ξ: multiplies the nonlinear phase (0: dispersion compensation only)";
341        amplification: String ["edfa"] one_of ["edfa", "ideal", "none"], "the amplification of the forward link";
342    }
343}
344
345struct ManakovDbpBlock(comnum_dsp::compensation::dbp::Dbp);
346
347impl ManakovDbpBlock {
348    fn new(p: &DbpSpec, ctx: &BuildContext) -> Result<ManakovDbpBlock> {
349        let link = ManakovSsfm {
350            amplification: p.amplification.parse()?,
351            ..ManakovSsfm::new(ctx.fiber(fiber_json(p.fiber.as_ref()))?)
352        };
353        Ok(ManakovDbpBlock(link.dbp(p.steps_per_span, p.nonlinear_scale)))
354    }
355}
356
357impl Block for ManakovDbpBlock {
358    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
359        ensure(s.n_streams() <= 2, || "manakov_dbp back-propagates one or two polarisations".into())?;
360        let fs = s.fs();
361        let n = s.samples.len();
362        if s.antennas.is_empty() {
363            let mut y = vec![C64::new(0.0, 0.0); n];
364            self.0.run_dual(&mut s.samples, &mut y, fs)
365        } else {
366            let (x, rest) = (&mut s.samples, &mut s.antennas);
367            self.0.run_dual(x, &mut rest[0], fs)
368        }
369    }
370}
371
372// ------------------------------------------------------------------------- wdm_rx
373
374block_params! {
375    struct WdmRx {
376        sps: usize [2] >= 1, "output samples per symbol";
377    }
378}
379
380impl Block for WdmRx {
381    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
382        let w = s.in_flight.wdm.take().ok_or_else(|| Error::Config("wdm_rx needs a wdm_tx before it".into()))?;
383        let sps_in = s.int_sps("wdm_rx")?;
384        ensure(self.sps >= 1 && self.sps <= sps_in, || {
385            format!("wdm_rx: sps must be between 1 and the {sps_in} samples per symbol of the multiplex")
386        })?;
387        let fs = s.fs();
388        let f = w.freqs_hz[w.cut];
389        let n_out = s.samples.len() / sps_in * self.sps;
390        for x in s.streams_mut() {
391            *x = wdm_demultiplex(x, f, fs, n_out);
392        }
393        s.sps = self.sps as f64;
394        Ok(())
395    }
396}