Fiber Links#
- class FiberLink(N_spans: int = 1, *, L_span: float = 80, StPS: int = 1, fs: float = 1, fiber: ~comnumpy.optical.fiber.FiberSpec = <factory>, NF_dB: float = 4, noise_scaling: float = 1, step_type: ~typing.Literal['linear', 'logarithmic'] = 'linear', step_method: ~typing.Literal['symmetric', 'asymetric'] = 'symmetric', use_only_linear: bool = False, step_log_factor: float = 0.4, name: str = 'fiber link', callbacks: ~typing.Dict[str, ~typing.Callable[[...], None]] = <factory>, raman: ~comnumpy.optical.raman.RamanSolution | None = None, seed: int | None = None)[source]#
Multi-span fiber link simulated with the split-step Fourier method (SSFM).
Signal Model#
Each span integrates the nonlinear Schroedinger equation (NLSE) for the complex field envelope \(A(z, t)\):
\[\frac{\partial A(z, t)}{\partial z} = -\frac{\alpha}{2} A(z, t) - j \frac{\beta_2}{2} \frac{\partial^2 A(z, t)}{\partial t^2} + j \gamma |A(z, t)|^2 A(z, t)\]where \(\alpha = \frac{\ln 10}{10} \alpha_{dB}\) is the attenuation (Np/km), \(\beta_2 = -10^3 D \lambda^2 / (2 \pi c)\) the group velocity dispersion (ps^2/km) and \(\gamma\) the Kerr coefficient (rad/W/km). The NLSE is integrated numerically with
StPSsplit steps per span (symmetric scheme: \(D(\delta z/2)\,N(\delta z)\,D(\delta z/2)\); asymmetric scheme: \(N(\delta z)\,D(\delta z)\)). After each span, an EDFA compensates the loss with power gain \(G\) and adds circular ASE noise of variance \(\sigma^2_{\mathrm{ase}}\):\[G = 10^{\alpha_{dB} L_{\mathrm{span}} / 10}, \qquad \sigma^2_{\mathrm{ase}} = \kappa \, f_s \, (G - 1) \, h \nu \, n_{sp}, \qquad n_{sp} = \frac{\mathrm{NF} / 2}{1 - 1/G}\]with \(\mathrm{NF} = 10^{\mathrm{NF_{dB}} / 10}\) the amplifier noise figure and \(\kappa\) a noise scaling factor.
Two polarizations (Manakov). A field shaped
(..., 2, N)– the antenna/channel axis of D2 – is propagated with the Manakov equation instead:\[\frac{\partial E_{x,y}}{\partial z} = -\frac{\alpha}{2} E_{x,y} - j \frac{\beta_2}{2} \frac{\partial^2 E_{x,y}}{\partial t^2} + j \frac{8\gamma}{9} \left(|E_x|^2 + |E_y|^2\right) E_{x,y}\]The two polarizations share the total intensity, and the \(8/9\) comes from averaging the nonlinearity over the fast random birefringence of a real fibre. It belongs to the model, not to the fibre:
fiber.gammastays the fibre’s Kerr coefficient in both modes, and which equation is integrated is read off the shape of the field, exactly as which pumps are on is read off their powers (D41). A(..., 1, N)field is a single polarization and keeps the scalar NLSE with \(\gamma\).Axes: declared axis – a full-field signal
(N,)or(..., P, N)withPin{1, 2}polarizations. The leading axes are a batch:(B, P, N)propagatesBrealizations in one call, which is 1.8x faster than the sameBcalls at 12288 samples and 3.1x at 1024 – the split-step loop, the per-span amplifier and the parameter precomputation are paid once instead ofBtimes. A batch of single-polarization fields is(B, 1, N); a bare(B, N)readsBas polarizations – refused for \(B \ge 3\), while(2, N)is read as one polarization pair by convention.- param N_spans:
Number of spans in the fiber link. Default is 1.
- type N_spans:
int
- param L_span:
Span length \(L_{\mathrm{span}}\) in km. Default is 80.
- type L_span:
float, keyword-only
- param StPS:
Number of split steps \(\delta z\) per span. Default is 1.
- type StPS:
int, keyword-only
- param fs:
Sampling frequency \(f_s\) in Hz. Default is 1.
- type fs:
float, keyword-only
- param NF_dB:
EDFA noise figure \(\mathrm{NF_{dB}}\) in dB. Default is 4.
- type NF_dB:
float, keyword-only
- param noise_scaling:
ASE noise scaling factor \(\kappa\) (0 disables the ASE noise). Default is 1.
- type noise_scaling:
float, keyword-only
- param step_type:
Step-size distribution within a span. Default is
"linear".- type step_type:
{“linear”, “logarithmic”}, keyword-only
- param step_method:
Split-step scheme. Default is
"symmetric".- type step_method:
{“symmetric”, “asymetric”}, keyword-only
- param use_only_linear:
If True, only the linear effects (CD and attenuation) are simulated, in a single exact step per span. Default is False.
- type use_only_linear:
bool, keyword-only
- param fiber:
The physical fibre (D46): loss \(\alpha_{dB}\), Kerr coefficient \(\gamma\), dispersion \(D\) and the wavelength the carrier frequency is derived from. Default is a standard single-mode fibre,
FiberSpec().- type fiber:
FiberSpec, keyword-only
- param step_log_factor:
Adjustment factor of the logarithmic step-size distribution. Default is 0.4.
- type step_log_factor:
float, keyword-only
- param name:
Name of the link instance. Default is
"fiber link".- type name:
str, optional, keyword-only
- param callbacks:
Hooks called during propagation; the key
"post_span"is called after each span ascallback(y, num_span=...).- type callbacks:
dict of str to callable, optional, keyword-only
- param raman:
Distributed Raman gain profile, from
solve_raman()over this span length (decision D45). Its on-off gain \(G_\mathrm{on\text{-}off}(z)\) is applied step by step inside the split-step loop, so the Kerr term sees the power the fibre actually carries – which is the whole reason a profile is used rather than a lumped gain. The EDFA is reduced by the same amount, so a span stays transparent whether or not it is pumped, and the ASE the solver integrated is added once per span, scaled from its reference bandwidth tofs.- type raman:
RamanSolution, optional, keyword-only
- param seed:
Seed of the local generator, which feeds both the Raman ASE and the per-span EDFA seed. Overridden by chain seeding (
Sequential.seed, D6).- type seed:
int, optional, keyword-only
- raman_step_gain_#
Amplitude gain applied at each split-step, read off the Raman profile at the step boundaries. Derived from the inputs, hence the trailing underscore (D23).
- Type:
np.ndarray
- raman_sigma2_#
Variance of the Raman ASE added at the end of each span, in the simulated bandwidth \(f_s\).
- Type:
float
- raises ShapeError:
If the input is neither a full-field 1D signal
(N,)nor a polarization pair(..., P, N)withPin{1, 2}(validated inprepare()): a pointwise Kerr step on a multi-channel array would silently produce SPM only (no XPM, no FWM).
References
G. P. Agrawal, Nonlinear Fiber Optics, 5th ed., Academic Press, 2013, Sections 3.2 (CD) and 4.1 (SPM).
J. Shao, X. Liang and S. Kumar, “Comparison of Split-Step Fourier Schemes for Simulating Fiber Optic Communication Systems,” IEEE Photonics Journal, vol. 6, no. 4, pp. 1-15, Aug. 2014, Art no. 7200515, doi: 10.1109/JPHOT.2014.2340993.
O. V. Sinkin, R. Holzlohner, J. Zweck and C. R. Menyuk, “Optimization of the split-step Fourier method in modeling optical-fiber communications systems,” Journal of Lightwave Technology, vol. 21, no. 1, pp. 61-68, Jan. 2003.
R.-J. Essiambre, G. Kramer, P. J. Winzer, G. J. Foschini and B. Goebel, “Capacity limits of optical fiber networks,” Journal of Lightwave Technology, vol. 28, no. 4, pp. 662-701, 2010.
Examples
>>> rng = np.random.default_rng(0) >>> x = 1e-3 * (rng.normal(size=128) + 1j * rng.normal(size=128)) >>> link = FiberLink(2, L_span=80.0, StPS=4, fs=10e9, noise_scaling=0) >>> y = link(x) >>> print(y.shape, y.dtype) (128,) complex128
- budget(bandwidth_Hz: float, *, polarizations: int = 1) Dict[str, Any][source]#
The amplifier noise this link accumulates, in closed form.
Signal Model#
Each of the \(N_s\) spans adds one amplifier’s worth of spontaneous emission, and the noise a receiver sees is that density integrated over the bandwidth it keeps – the symbol rate, for a matched filter:
\[P_{\mathrm{ASE}} = n_{\mathrm{pol}} \, N_s \, N_{\mathrm{ASE}} \, B, \qquad N_{\mathrm{ASE}} = (G - 1) h \nu \, n_{sp}, \qquad n_{sp} = \frac{\mathrm{NF}/2}{1 - 1/G}\]with \(n_{\mathrm{pol}}\) the number of polarizations (\(P\) denotes power everywhere else in this module). That factor is the whole reason this method exists. \(N_{\mathrm{ASE}}\) is a density per polarization, and a dual-polarization link carries two independent copies of it; quoting a channel power against a single-polarization budget is a 3 dB error that looks exactly like a modelling disagreement. Writing the product by hand at each call site is how the two conventions drift apart.
The result is the link’s own prediction: it owes nothing to a simulation, so it is the reference a measured effective SNR is checked against.
noise_scalingis applied, so a link with its noise switched off budgets zero.- param bandwidth_Hz:
Bandwidth the receiver collects, in Hz. For a matched filter this is the symbol rate, not the simulated
fs.- type bandwidth_Hz:
float
- param polarizations:
1 (default) for a single-polarization link, 2 for the dual-polarization link a coherent system uses – the same convention as
gn_model_nli_power().- type polarizations:
int, optional, keyword-only
- returns:
dict –
ase_power_Wandase_power_dBm, the accumulated noise;ase_density_W_per_Hz, one span’s density per polarization; and the inputs it was built from (n_spans,NF_dB,span_gain_dB,bandwidth_Hz,polarizations).- raises ValueError:
If the bandwidth is not positive, or if
polarizationsis neither 1 nor 2: a fibre carries one or two.
Examples
Ten 100 km spans with a 5 dB noise figure, one polarization, read in a 32 GBd matched filter:
>>> link = FiberLink(10, L_span=100.0, NF_dB=5.0, fs=192e9) >>> budget = link.budget(32e9) >>> print(f"{budget['ase_power_dBm']:.2f} dBm") -21.88 dBm
The second polarization carries its own noise, and nothing else changes:
>>> print(f"{link.budget(32e9, polarizations=2)['ase_power_dBm']:.2f} dBm") -18.87 dBm