1use 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#[derive(Clone, Debug)]
88pub struct WdmFrame {
89 pub freqs_hz: Vec<f64>,
91 pub cut: usize,
93}
94
95pub(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
141block_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 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
217block_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
294block_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
304struct 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 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
334block_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
372block_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}