GN model#
The Gaussian Noise model: what a fibre link costs, in closed form.
A split-step simulation answers “what does this link do?” one launch power at a time, and each answer costs minutes. The GN model answers “how much nonlinear noise does this link produce?” in closed form, in microseconds, and it is the reference every fibre simulator is checked against.
Its assumption is in its name. After enough uncompensated dispersion the propagating signal looks Gaussian, so the nonlinear interference (NLI) it generates on itself behaves like additive Gaussian noise whose power grows with the cube of the launch power. The link is then an additive-noise channel with two terms:
and everything an engineer wants – the optimal launch power, the peak
SNR, the reach – follows from that one expression (see
optimal_launch_power()).
Which equation the fibre is integrating matters. The 16/27 the
model is built on is a Manakov coefficient: it assumes a
dual-polarization signal in a fibre whose birefringence averages the
nonlinearity, which is what a real coherent link does and what
FiberLink integrates when the field is
shaped (..., 2, N). Hand a one-dimensional field to the same block
and it integrates the scalar NLSE instead, which produces
3.375 times more nonlinear interference at equal total power. That
is not a small print: it is 5.3 dB, and it is the difference between a
simulation that confirms this module and one that appears to refute it.
Pass polarizations=1 to gn_model_nli_power() in that case –
the weights become 2 and 4 rather than 16/27 and 32/27, which is
Table I of Serena and Bononi (2015) with the 8/9 removed because the
scalar equation uses \(\gamma\) directly.
What it is not. The Gaussian assumption is also the model’s known
bias: a real modulation format is not Gaussian, its fourth moment is
smaller, and the true NLI is lower than the GN model predicts. The
correction is the EGN model, and it is deliberately not
implemented here – see the note at the end of
validation/optical_gn_model.py, which measures the effect instead
of predicting it, and reports it against the published values below.
References
P. Poggiolini, “The GN model of non-linear propagation in uncompensated coherent optical systems”, J. Lightwave Technol., vol. 30, no. 24, pp. 3857-3879, 2012. The published paper and its extended version number their equations differently, so both are given here: the compact single-span form is Eq. (15) of the published paper, while the per-pair sum and the per-pair psi implemented below are eq. 120 and 123 of the extended version, arXiv:1209.0394. A post-print is at https://iris.polito.it/handle/11583/2503175 ; P. Poggiolini, G. Bosco, A. Carena, V. Curri, Y. Jiang, F. Forghieri, “The GN-model of fiber non-linear propagation and its applications”, J. Lightwave Technol., vol. 32, no. 4, pp. 694-721, 2014; P. Serena and A. Bononi, “A time-domain extended Gaussian noise model”, J. Lightwave Technol., vol. 33, no. 7, pp. 1459-1472, 2015 – Table I for the polarization weights, and Section III for the published benchmark this module is pinned to; A. Carena, G. Bosco, V. Curri, Y. Jiang, P. Poggiolini, F. Forghieri, “EGN model of non-linear fiber propagation”, Opt. Express, vol. 22, no. 13, pp. 16335-16362, 2014 (the modulation-format correction this module does not implement).
The published number this module reproduces. Serena and Bononi
(2015), Section III, simulate 15 channels at 32 GBd on a 37.5 GHz grid
over 5 x 100 km of SMF and report the normalized NLI coefficient
\(a_{\mathrm{NL}}\), defined by
\(\sigma^2_{\mathrm{NLI}} = a_{\mathrm{NL}} P^3\), as -23.5 dB
(mW^-2) for a Gaussian-modulated signal – the case the GN model is
written for. This module returns -23.30 dB for that link, an agreement
of 0.2 dB against an independent split-step Monte Carlo simulation
published by a different group; the check is
tests/optical/test_gn_model.py::TestAgainstPublishedBenchmark.
The two implemented equations were cross-checked line by line against
the reference open-source implementation of the same paper, GNPy
(Telecom Infra Project, gnpy/core/science_utils.py,
NliSolver._gn_analytic and NliSolver._psi), which fixes the same
conventions this library already uses: \(\alpha\) is the power
attenuation coefficient, so \(P(z) = P(0)e^{-\alpha z}\) and
\(L_{\mathrm{eff}} = (1 - e^{-\alpha L})/\alpha\). GNPy works in
metres and s^2/m where this library works in kilometres and ps^2/km, so
the agreement is not a shared typo:
tests/optical/test_gn_model.py re-transcribes both GNPy routines in
SI units, from scratch, and requires the two to agree to twelve digits.
- gn_model_psi(delta_f_Hz: ndarray | float, baud_cut_Hz: float, baud_pump_Hz: ndarray | float, beta2_ps2_km: float, effective_length_km: float, asymptotic_length_km: float) ndarray[source]#
The \(\psi\) factor of the GN model, eq. 123.
Signal Model#
The nonlinear interference a pump channel deposits on a cut channel is an integral over the two spectra of the phase-matching condition. When both are approximated by rectangles, that integral is available in closed form:
\[\psi = \frac{L_{\mathrm{eff}}^2} {4 \pi \left|\beta_2\right| L_a} \left[ \mathrm{asinh}\left(\pi^2 L_a \left|\beta_2\right| R_{\mathrm{cut}} \left(\Delta f + \tfrac{R_{\mathrm{pump}}}{2}\right)\right) - \mathrm{asinh}\left(\pi^2 L_a \left|\beta_2\right| R_{\mathrm{cut}} \left(\Delta f - \tfrac{R_{\mathrm{pump}}}{2}\right)\right) \right]\]with \(L_a = 1/\alpha\) the asymptotic length and \(\Delta f\) the spacing between the two carriers. The \(\mathrm{asinh}\) is the signature of the model: it is why the NLI grows only logarithmically with the bandwidth, and why doubling the number of channels costs far less than doubling their power.
Axes: element-wise –
delta_f_Hzmay be an array; everything else is scalar.- param delta_f_Hz:
Carrier spacing \(\Delta f\) in Hz (0 for the channel on itself).
- type delta_f_Hz:
float or np.ndarray
- param baud_cut_Hz:
Symbol rate of the channel the NLI lands on, in Bd.
- type baud_cut_Hz:
float
- param baud_pump_Hz:
Symbol rate of the channel producing it, in Bd.
- type baud_pump_Hz:
np.ndarray or float
- param beta2_ps2_km:
Group-velocity dispersion in ps^2/km (the library’s unit; see
beta2).- type beta2_ps2_km:
float
- param effective_length_km:
\(L_{\mathrm{eff}} = (1 - e^{-\alpha L})/\alpha\), in km.
- type effective_length_km:
float
- param asymptotic_length_km:
\(L_a = 1/\alpha\), in km.
- type asymptotic_length_km:
float
- returns:
np.ndarray – \(\psi\), in km^2 s^-2 – the bracket is dimensionless and the prefactor carries the units, chosen so that dividing by a squared symbol rate and multiplying by \(\gamma^2\) in
gn_model_nli_power()leaves W^-2.- raises ValueError:
If the dispersion is zero: the model divides by it, and a dispersionless fibre has no phase mismatch to integrate over.
References
Poggiolini, JLT 30(24), 2012, eq. 123 (arXiv:1209.0394); GNPy
NliSolver._psi.Examples
>>> from comnumpy.optical.fiber import get_fiber >>> fiber = get_fiber("SMF") >>> alpha = fiber.alpha_per_km >>> psi = gn_model_psi(0.0, 32e9, 32e9, fiber.beta2, ... fiber.effective_length_km(80.0), 1 / alpha) >>> print(f"{float(psi):.4e}") 2.4259e+23
- gn_model_nli_power(fiber: FiberSpec, *, span_length_km: float, n_spans: int, powers_W: ndarray, frequencies_Hz: ndarray, baud_rates_Hz: ndarray, coherence_exponent: float = 0.0, polarizations: int = 2) ndarray[source]#
Nonlinear interference power of every channel of a comb, eq. 120.
Signal Model#
Each pair of channels contributes, and the contribution of the pump \(j\) on the cut \(i\) is cubic in the powers:
\[P_{\mathrm{NLI},i} = \sum_j \eta_{ij} \, P_i P_j^2, \qquad \eta_{ij} = \frac{\gamma^2 \, w_{ij} \, \psi_{ij}}{R_j^2}\]with \(w_{ii} = 16/27\) for the channel on itself and \(w_{ij} = 32/27\) for a neighbour – the factor two being the two ways a pair of pump photons can mix. \(\psi_{ij}\) is
gn_model_psi(). Those two numbers assume the Manakov equation, i.e. a dual-polarization signal; for a scalar simulation seepolarizationsbelow.Over \(N_s\) identical spans the standard approximation is incoherent accumulation, each span adding its own noise:
\[P_{\mathrm{NLI}}^{\mathrm{link}} = N_s^{1+\varepsilon} \, P_{\mathrm{NLI}}^{\mathrm{span}}\]\(\varepsilon = 0\) being the incoherent case and a small positive value the empirical correction for the partial coherence a long link develops.
Axes: reduces the pair axis – the comb is described by three arrays of one entry per channel, and one NLI power per channel comes back.
- param fiber:
Fibre of one span; only
alpha_per_km,beta2andgammaare read.- type fiber:
FiberSpec
- param span_length_km:
Length \(L\) of one span in km.
- type span_length_km:
float, keyword-only
- param n_spans:
Number of identical spans \(N_s\).
- type n_spans:
int, keyword-only
- param powers_W:
Launch power of each channel, in W.
- type powers_W:
np.ndarray, keyword-only
- param frequencies_Hz:
Carrier frequency of each channel, in Hz (only differences matter).
- type frequencies_Hz:
np.ndarray, keyword-only
- param baud_rates_Hz:
Symbol rate of each channel, in Bd.
- type baud_rates_Hz:
np.ndarray, keyword-only
- param coherence_exponent:
\(\varepsilon\) above. Default 0, the incoherent accumulation.
- type coherence_exponent:
float, optional, keyword-only
- param polarizations:
2 (default) for the dual-polarization Manakov link the model is written for, which is also what
FiberLinkintegrates when the field is shaped(..., 2, N). Pass 1 to compare against a scalar NLSE simulation – a one-dimensional field – whose weights are 2 and 4 instead, giving 3.375 times (5.3 dB) more interference at the same total power.- type polarizations:
int, optional, keyword-only
- returns:
np.ndarray – NLI power of each channel, in W, referred to the input of a span (the convention of the reference implementation: the link is lossless end to end once the amplifiers are counted).
- raises ValueError:
If the three arrays do not have the same length, if the number of spans is not positive, or if
polarizationsis neither 1 nor 2.
References
Poggiolini, JLT 30(24), 2012, eq. 120; GNPy
NliSolver._gn_analytic; Serena and Bononi, JLT 33(7), 2015, Table I for the two sets of weights.Examples
A single 32 GBd channel over one 80 km span of standard fibre, at 0 dBm:
>>> from comnumpy.optical.fiber import get_fiber >>> import numpy as np >>> power = np.array([1e-3]) >>> nli = gn_model_nli_power(get_fiber("SMF"), span_length_km=80.0, ... n_spans=1, powers_W=power, ... frequencies_Hz=np.array([193.4e12]), ... baud_rates_Hz=np.array([32e9])) >>> print(f"{10 * np.log10(nli[0] / 1e-3):.2f} dBm") -36.25 dBm
Eight neighbours on a 50 GHz grid make it worse, but far less than proportionally. If each neighbour hurt the centre channel as much as the channel hurts itself, the eight of them would add \(10\log_{10}(1 + 2 \times 8) = 12.3\) dB; the \(\mathrm{asinh}\) turns that into 4.3 dB:
>>> comb = 193.4e12 + 50e9 * np.arange(-4, 5) >>> nli_comb = gn_model_nli_power(get_fiber("SMF"), span_length_km=80.0, ... n_spans=1, powers_W=np.full(9, 1e-3), ... frequencies_Hz=comb, ... baud_rates_Hz=np.full(9, 32e9)) >>> print(f"{10 * np.log10(nli_comb[4] / 1e-3):.2f} dBm " ... f"({10 * np.log10(nli_comb[4] / nli[0]):.2f} dB worse)") -31.95 dBm (4.30 dB worse)
The same span integrated with the scalar equation instead, which is what a one-dimensional field gets, is 5.28 dB worse again:
>>> scalar = gn_model_nli_power(get_fiber("SMF"), span_length_km=80.0, ... n_spans=1, powers_W=power, ... frequencies_Hz=np.array([193.4e12]), ... baud_rates_Hz=np.array([32e9]), ... polarizations=1) >>> print(f"{10 * np.log10(scalar[0] / nli[0]):.2f} dB") 5.28 dB
- gn_model_snr(ase_power_W: float, nli_coefficient: float, power_W: ndarray | float) ndarray[source]#
The two-noise SNR of a fibre link.
Signal Model#
\[\mathrm{SNR}(P) = \frac{P}{P_{\mathrm{ASE}} + \eta P^3}\]The amplifiers contribute a power-independent term, the fibre a cubic one, and the tension between them is the whole design problem: turning the power up buys signal linearly and noise cubically.
Axes: element-wise in
power_W.- param ase_power_W:
Amplified spontaneous emission power in the channel bandwidth, in W.
- type ase_power_W:
float
- param nli_coefficient:
\(\eta\) such that the NLI power is \(\eta P^3\), in 1/W^2. It is
gn_model_nli_power()divided by \(P^3\).- type nli_coefficient:
float
- param power_W:
Launch power per channel, in W.
- type power_W:
float or np.ndarray
- returns:
np.ndarray – Signal-to-noise ratio, linear.
References
Poggiolini et al., JLT 32(4), 2014, Section IV.
Examples
>>> import numpy as np >>> snr = gn_model_snr(1e-6, 5e3, np.array([1e-4, 1e-3])) >>> print(np.round(10 * np.log10(snr), 2)) [19.98 22.22]
- optimal_launch_power(ase_power_W: float, nli_coefficient: float) tuple[float, float][source]#
The launch power that maximizes the SNR, and that SNR.
Signal Model#
Setting the derivative of \(P / (P_{\mathrm{ASE}} + \eta P^3)\) to zero gives a condition worth remembering in words – the optimum is where the nonlinear noise is half the ASE:
\[\eta P_{\mathrm{opt}}^3 = \frac{P_{\mathrm{ASE}}}{2}, \qquad P_{\mathrm{opt}} = \left(\frac{P_{\mathrm{ASE}}}{2\eta}\right)^{1/3}, \qquad \mathrm{SNR}_{\mathrm{max}} = \frac{2}{3}\, \frac{P_{\mathrm{opt}}}{P_{\mathrm{ASE}}}\]Two consequences follow immediately, and both are checked in
tests/optical/test_gn_model.pyrather than asserted here.First, the peak is asymmetric, because one side of it is governed by a linear term and the other by a cubic one. Missing the optimum by 1 dB costs 0.24 dB of SNR if the error is upwards but only 0.21 dB if it is downwards; by 3 dB, 2.20 dB against 1.50 dB. A link is therefore run slightly under its optimum, where a power error is the cheaper kind.
Second, since \(P_{\mathrm{opt}} \propto P_{\mathrm{ASE}}^{1/3}\), ten times the accumulated ASE – ten times the spans, near enough – moves the optimum by 10/3 dB, not by 10.
- param ase_power_W:
ASE power in the channel bandwidth, in W.
- type ase_power_W:
float
- param nli_coefficient:
\(\eta\) in 1/W^2, as in
gn_model_snr().- type nli_coefficient:
float
- returns:
tuple of float – Optimal launch power in W, and the SNR reached there (linear).
- raises ValueError:
If either argument is not positive: without ASE the optimum is zero power, without nonlinearity it is infinite, and neither is a design point.
References
Poggiolini et al., JLT 32(4), 2014, eq. 26-27.
Examples
>>> import numpy as np >>> power, snr = optimal_launch_power(1e-6, 5e3) >>> print(f"{10 * np.log10(power / 1e-3):.2f} dBm, " ... f"{10 * np.log10(snr):.2f} dB") -3.33 dBm, 24.91 dB
The condition, checked rather than asserted:
>>> print(round(5e3 * power ** 3 / (1e-6 / 2), 12)) 1.0