Raman amplification#
Distributed Raman amplification: gain spectra and the pump equations.
Two objects and one solver, split the way the physics is:
RamanGainSpectrum– the shape of the Raman gain against the pump-signal frequency shift, normalized to 1 at its peak, with its provenance. It is a property of the glass, so it is catalogued the way D15/D20/D43 catalogue the frequency and delay axes: frozen value object, registry, and a self-check at construction against the figures its source publishes.the peak coefficient \(g_R / A_\mathrm{eff}\) is not in that object. It is a property of the fibre – SMF, DCF and NZDSF differ by a large factor through the effective area and the germanium doping – so it is an argument of the solver, next to the losses.
solve_raman()– the coupled power equations, integrated forwards when only the co-propagating pump is on and solved as a two-point boundary value problem otherwise.
There is no Processor here, on purpose. Raman lives in the power
domain and the split-step propagation of
FiberLink lives in the field domain:
a block that multiplied the field by a lumped gain at the end of a span
would describe a discrete amplifier, which is the one thing distributed
amplification is not. What this module produces is the gain profile
\(G(z)\), meant to be applied inside the linear step.
- class RamanGainSpectrum(*, lorentzian: tuple[float, float] | None = None, triangular: float | None = None, tabulated: tuple[ndarray, ndarray] | None = None, quoted_at: tuple[float, float, float] | None = None, standard: str = 'custom', reference: str = '')[source]#
Normalized Raman gain shape against the pump-signal frequency shift.
Signal Model#
Stimulated Raman scattering transfers power from a pump at \(\nu_p\) to a signal at \(\nu_s < \nu_p\), with an efficiency that depends only on the Stokes shift
\[\Delta \nu = \nu_p - \nu_s\]This object carries the shape \(\tilde{g}(\Delta \nu)\), normalized so that \(\max_{\Delta \nu} \tilde{g} = 1\); the fibre’s peak coefficient \(g_R / A_\mathrm{eff}\) multiplies it (see
solve_raman()). Exactly one of the two closed forms below is selected, the D41 way – one keyword per parameterization, no discriminator argument.Damped-oscillator model (
lorentzian). The Raman response of silica is modelled by a single damped oscillator of period \(\tau_1\) and lifetime \(\tau_2\), whose Fourier transform is\[\tilde{H}_R(\omega) = \frac{\tau_1^{-2} + \tau_2^{-2}} {\tau_1^{-2} - \left(\omega + i \tau_2^{-1}\right)^2}, \qquad \omega = 2 \pi \Delta \nu\]and the gain is \(\operatorname{Im} \tilde{H}_R\), normalized here to its peak. \(\tilde{H}_R(0) = 1\) fixes the normalization of the response itself.
Triangular model (
triangular). Over the first few terahertz the gain is linear in the shift, which is what the closed-form inter-channel Raman tilt of a WDM comb relies on:\[\begin{split}\tilde{g}(\Delta \nu) = \begin{cases} \Delta \nu / \Delta \nu_\mathrm{peak} & 0 \leq \Delta \nu \leq \Delta \nu_\mathrm{peak}\\ 0 & \text{otherwise} \end{cases}\end{split}\]Normalizing to the peak leaves a single free number, \(\Delta \nu_\mathrm{peak}\): the slope is then \(1/\Delta \nu_\mathrm{peak}\) in normalized units and the absolute slope comes from the fibre’s peak coefficient.
- param lorentzian:
\((\tau_1, \tau_2)\) in femtoseconds. Mutually exclusive with
triangular.- type lorentzian:
tuple of two floats, optional, keyword-only
- param triangular:
Peak Stokes shift \(\Delta \nu_\mathrm{peak}\) in THz. Mutually exclusive with
lorentzian.- type triangular:
float, optional, keyword-only
- param standard:
Name of the model, e.g.
"Blow-Wood".- type standard:
str, keyword-only
- param reference:
Publication the parameters were copied from (same provenance rule as D15/D20/D43).
- type reference:
str, keyword-only
- raises ValueError:
If neither or both parameterizations are given, or if a time constant or the peak shift is not positive, or if any of them is off by orders of magnitude from its stated unit. The last check is there because the units differ between parameterizations – femtoseconds here, terahertz there, hertz in the table – and a slip is silent otherwise:
triangular=13.2e12is positive and valid-looking, and yields a spectrum that rises linearly across the whole band instead of peaking at 13.2 THz. The bounds are wide enough that no real glass or fibre reaches them, so what they reject is a scale mistake rather than an unusual design.
References
K. J. Blow, D. Wood, “Theoretical description of transient stimulated Raman scattering in optical fibers”, IEEE J. Quantum Electron., vol. 25, no. 12, pp. 2665-2673, 1989.
G. P. Agrawal, Nonlinear Fiber Optics, 5th ed., Academic Press, 2013, Sections 2.3 and 8.1.
Examples
>>> spectrum = get_gain_spectrum("blow-wood") >>> round(spectrum.peak_shift_THz, 2) 13.08 >>> round(float(spectrum.shape(13.08e12)), 6) 1.0
- pair_scaling(frequency_Hz: ndarray) ndarray[source]#
Waveguide correction of the gain, per (receiver, partner) pair.
The gain
shape()is a property of the glass and depends on the Stokes shift alone. The coefficient that multiplies \(P_i P_j\) in a power equation is not that shape but \(g_R / A_{\mathrm{eff}}\), and the effective area is a property of the waveguide: the mode spreads as the wavelength grows, so the same Stokes shift buys less gain further into the infrared. With a Gaussian mode this reduces to a one-parameter law, \(A_{\mathrm{eff}}(\nu) = A_0 k / (\ln(\nu/\nu_0) + k)\) with \(k = \pi a^2 / A_0\). A pair is charged the mean of the two areas, and the gain is referred back to the frequency the table was quoted at.Across one band this is a few per cent; across C+L+S it is the difference between a flat prediction and a wrong tilt.
Axes: returns a matrix
(n, n), one factor per ordered pair, withjthe partner that feedsi.- Parameters:
frequency_Hz (array_like) – Frequencies of the waves, in Hz.
- Returns:
np.ndarray – Scaling factors, all 1 when the spectrum does not record where it was quoted.
References
The Gaussian-mode form \(w = a/\sqrt{\ln V}\) and the arithmetic-mean overlap are transcribed from GNPy (
gnpy/core/parameters.py,effective_area_scalingandeffective_area_overlap, BSD-3-Clause), which does not give a source for them; the one-parameter law above is that model with the index contrast eliminated, and is derived invalidation/optical_raman_gnpy.pyrather than taken from a paper. The gain profile it is applied to comes from A. D’Amico et al., J. Lightwave Technol., vol. 40, pp. 3499-3511, 2022, Section III.D.The confirmation that matters is empirical rather than bibliographic: with this law the coupling coefficients match GNPy’s element by element on the gain side, ratio 1.0000.
Examples
>>> spectrum = get_gain_spectrum("blow-wood") >>> float(np.max(np.abs(spectrum.pair_scaling( ... np.array([193e12, 206e12])) - 1.0))) 0.0
- shape(shift_Hz: ndarray) ndarray[source]#
Normalized gain \(\tilde{g}(\Delta \nu)\), peak value 1.
- Parameters:
shift_Hz (array_like) – Stokes shift \(\Delta \nu = \nu_p - \nu_s\) in Hz. A negative shift means the “signal” is above the pump, where the model gives no gain.
- Returns:
np.ndarray – Gain shape, between 0 and 1.
Examples
>>> spectrum = get_gain_spectrum("triangular") >>> [round(float(spectrum.shape(f)), 3) for f in (0.0, 6e12, -6e12)] [0.0, 0.455, 0.0]
- property peak_shift_THz: float#
Stokes shift \(\Delta \nu_\mathrm{peak}\) of the maximum.
- property fwhm_THz: float#
Full width at half maximum of the gain shape, in THz.
- class RamanSolution(z_km: ndarray, wave_power_W: ndarray, spontaneous_per_wave: ndarray, signal_W: ndarray, pump_forward_W: ndarray, pump_backward_W: ndarray, ase_W: ndarray, loss_only_W: ndarray, bandwidth_Hz: float, frequency_signal_Hz: float | ndarray, _pump_transmission: float | ndarray = 1.0)[source]#
Power profiles along the span, and what is read off them.
Signal Model#
All profiles are functions of the distance \(z\) from the span input. Per signal \(i\), the figures of merit are
\[G_\mathrm{on\text{-}off} = \frac{P_{s,i}(L)}{P_{s,i}(L)\big|_{P_p = 0}} = \frac{P_{s,i}(L)}{P_{s,i}(0) e^{-\alpha_{s,i} L}}, \qquad G_\mathrm{net} = \frac{P_{s,i}(L)}{P_{s,i}(0)}\]– the on-off gain is what the pumps buy, the net gain is what comes out of the span – and the effective noise figure
\[F_\mathrm{eff} = \frac{1}{G_\mathrm{net}} \left(1 + \frac{P_\mathrm{ASE}(L)}{h \nu_s B}\right)\]which is effective because it refers the noise of the whole distributed span to a hypothetical discrete amplifier; it may be below 0 dB, and that is the reason distributed amplification is used at all.
Shapes. Each profile carries one row per wave,
(n, n_z), except when its group holds a single wave: a solve with one signal and one pump wavelength returns the plain(n_z,)curves, so the single-channel case reads exactly as it did before the multi-wave generalization. The figures of merit follow the same rule: a float for one signal, an array of one value per signal otherwise.- z_km#
Distance grid, in km, from 0 to the span length.
- Type:
np.ndarray
- signal_W#
Signal powers \(P_{s,i}(z)\) in W, one row per signal.
- Type:
np.ndarray
- pump_forward_W, pump_backward_W
Pump powers \(P_p^{+}(z)\) and \(P_p^{-}(z)\) in W, one row per pump wavelength. Both are given on the same \(z\) grid; the backward pumps are launched at \(z = L\). A pump that is off in one direction is stored as a zero row, so the two arrays always have the same shape.
- Type:
np.ndarray
- ase_W#
Amplified spontaneous emission \(P_\mathrm{ASE}(z)\) in W, one row per signal, in the reference bandwidth of the solve.
- Type:
np.ndarray
- bandwidth_Hz#
Reference bandwidth \(B\) the ASE was integrated over.
- Type:
float
- frequency_signal_Hz#
Signal frequencies \(\nu_{s,i}\), needed to turn the ASE into a noise figure.
- Type:
np.ndarray or float
- loss_only_W#
The signal profiles the same fibre would give with the pumps off, \(P_{s,i}(0) e^{-\alpha_{s,i} z}\). Carrying them makes the on-off gain a ratio of two stored curves rather than a recomputation.
- Type:
np.ndarray
Examples
>>> solution = solve_raman(length_km=80.0, gain_peak_W_km=0.4, ... pump_backward_W=0.2, signal_W=1e-3) >>> round(solution.on_off_gain_dB, 2) 5.97
- property ase_from_pumps_W: ndarray#
ASE the pumps seed, which is what a Raman amplifier is charged for.
- property ase_from_signals_W: ndarray#
ASE the channels seed in each other, which pump-only models omit.
Every channel sits below some of its neighbours, so the comb seeds itself. The shifts involved are small – a few terahertz across a band – where the Bose occupancy \(1/(e^{h \Delta \nu / k T} - 1)\) is of order one rather than negligible, so the term is thermally enhanced. Planning tools commonly drop it; on a 96-channel C-band comb it is worth about 0.17 dB of ASE.
- property n_signals: int#
Number of signal waves the solve carried.
- property n_pumps: int#
Number of pump wavelengths (a bidirectional pump counts once).
- property on_off_gain_dB: float | ndarray#
output with pumps over output without.
- Type:
Gain the pumps buy, in dB
- property net_gain_dB: float | ndarray#
Span output over span input, in dB. Negative for a lossy span.
- property gain_profile_dB: ndarray#
On-off gain accumulated up to each \(z\), in dB.
This is the curve the split-step loop of
FiberLinkconsumes – one row per signal, so a WDM link applies its own tilted gain to each channel of the axis D44 gives it.
- property tilt_dB: float#
Spread of the on-off gain across the signals, in dB.
Zero for a single signal. This is the number a multi-pump design exists to minimize: one pump amplifies the red end of the comb and starves the blue end, two well-placed pumps flatten it.
- property noise_figure_dB: float | ndarray#
Effective noise figure of the span, in dB; may be negative.
- property pump_depletion: float#
Fraction of the launched pump power the signals took away.
Zero in the undepleted regime the analytic gain formula assumes; a large value is the warning that it no longer holds. Summed over the pumps: a per-pump breakdown is in the profiles.
- solve_raman(*, length_km: float, gain_peak_W_km: float, signal_W: ArrayLike = 0.001, pump_forward_W: ArrayLike = 0.0, pump_backward_W: ArrayLike = 0.0, alpha_signal_dB_km: ArrayLike = 0.2, alpha_pump_dB_km: ArrayLike = 0.25, wavelength_signal_nm: ArrayLike = 1550.0, wavelength_pump_nm: ArrayLike = 1455.0, spectrum: RamanGainSpectrum | None = None, bandwidth_Hz: float = 12500000000.0, temperature_K: float = 300.0, n_nodes: int = 401, tol: float = 1e-08) RamanSolution[source]#
Solve the coupled Raman power equations along one span.
Signal Model#
Every wave – each signal channel, each pump, in each direction – is one power \(P_i(z)\) written in the \(+z\) coordinate, with a direction \(d_i = +1\) for a co-propagating wave and \(-1\) for a counter-propagating one. They obey
\[\frac{\mathrm{d} P_i}{\mathrm{d} z} = d_i \left[\sum_{j} C_{ij} P_j - \alpha_i \right] P_i\]where the pairwise coupling is set by the gain shape at the shift separating the two waves,
\[C_{ij} = \frac{g_R}{A_\mathrm{eff}}\, \tilde{g}(\nu_j - \nu_i) \quad (\nu_j > \nu_i), \qquad C_{ji} = -\frac{\nu_j}{\nu_i} C_{ij}\]A pair therefore always exchanges photons one for one, the energy difference going into a phonon, and that holds for every pair: pump to signal, pump to pump, and – the reason a WDM comb tilts – the blue channels of the comb to its own red ones.
Spontaneous emission adds, for each signal,
\[\frac{\mathrm{d} P_{\mathrm{ASE},i}}{\mathrm{d}z} = d_i \left[\left(\sum_j C_{ij} P_j - \alpha_i\right) P_{\mathrm{ASE},i} + 2 h \nu_i B \sum_{j:\, C_{ij} > 0} C_{ij} P_j \left(1 + \eta(\nu_j - \nu_i, T)\right)\right]\]the last bracket being the phonon occupancy, which is why a Raman amplifier is quieter when it is cold. The ASE is amplified but does not itself deplete the pumps.
Boundary conditions. Co-propagating waves are known at the span input, counter-propagating ones at its output:
\[P_i(0) = P_i^\mathrm{in} \ (d_i = +1), \quad P_i(L) = P_i^\mathrm{in} \ (d_i = -1), \quad P_\mathrm{ASE}(0) = 0\]With no counter-propagating pump everything is known at \(z=0\) and the system is an initial value problem, integrated with
scipy.integrate.solve_ivp. Otherwise it is a genuine two-point boundary value problem andscipy.integrate.solve_bvpis used, started from the undepleted-pump profiles – a poor initial guess is the usual reason that solver fails to converge.Undepleted limit. For one signal and one pump, when the signal takes a negligible part of the pump, the pump decays exponentially and the on-off gain has the closed form
\[G_\mathrm{on\text{-}off} = \exp\left(g P_p L_\mathrm{eff}\right), \qquad L_\mathrm{eff} = \frac{1 - e^{-\alpha_p L}}{\alpha_p}\]which is what
validation/optical_raman.pychecks the solver against, along with photon-number conservation in the lossless limit.Multi-wave solves. Every argument describing a signal accepts either a scalar – shared by the whole comb – or one value per channel, and likewise for the pumps; a scalar in gives a scalar out. Since a shift now separates every pair,
spectrum=becomes required as soon as there is more than one signal or more than one pump wavelength: its default meaning, “the pair sits at the gain peak”, is only defensible for a single pair.Axes: not a Processor – this is a power-domain solver, and it returns profiles rather than transforming a signal. See the module docstring.
- param length_km:
Span length \(L\), in km.
- type length_km:
float, keyword-only
- param gain_peak_W_km:
Peak Raman gain coefficient \(g_R / A_\mathrm{eff}\) of the fibre, in \(\mathrm{W}^{-1}\mathrm{km}^{-1}\). A property of the fibre, not of the glass spectrum.
- type gain_peak_W_km:
float, keyword-only
- param signal_W:
Launched signal powers \(P_\mathrm{in}\), in W. A scalar is the same launch power on every channel.
- type signal_W:
float or array_like, optional, keyword-only
- param pump_forward_W:
Co-propagating pump powers \(P_f\) launched at \(z=0\), in W, one per pump wavelength.
- type pump_forward_W:
float or array_like, optional, keyword-only
- param pump_backward_W:
Counter-propagating pump powers \(P_b\) launched at \(z = L\), in W, one per pump wavelength. A pump may be on in both directions. There is no
directionargument: which pumps are on is the configuration (D41).- type pump_backward_W:
float or array_like, optional, keyword-only
- param alpha_signal_dB_km:
Fibre attenuation \(\alpha_s\) and \(\alpha_p\) at the signal and pump wavelengths, in dB/km.
- type alpha_signal_dB_km:
float or array_like, optional, keyword-only
- param alpha_pump_dB_km:
Fibre attenuation \(\alpha_s\) and \(\alpha_p\) at the signal and pump wavelengths, in dB/km.
- type alpha_pump_dB_km:
float or array_like, optional, keyword-only
- param wavelength_signal_nm:
Wavelengths setting \(\nu_s\), \(\nu_p\) and hence every Stokes shift \(\Delta \nu\). The lengths of the signal arguments fix the number of channels, those of the pump arguments the number of pumps.
- type wavelength_signal_nm:
float or array_like, optional, keyword-only
- param wavelength_pump_nm:
Wavelengths setting \(\nu_s\), \(\nu_p\) and hence every Stokes shift \(\Delta \nu\). The lengths of the signal arguments fix the number of channels, those of the pump arguments the number of pumps.
- type wavelength_pump_nm:
float or array_like, optional, keyword-only
- param spectrum:
Gain shape used to scale
gain_peak_W_kmat each pair’s shift.None(default) uses the peak coefficient as given, i.e. assumes the pump sits at the gain peak, and is only accepted for a single signal and a single pump wavelength.- type spectrum:
RamanGainSpectrum, optional, keyword-only
- param bandwidth_Hz:
Reference bandwidth \(B\) for the ASE. Default 12.5 GHz, the 0.1 nm convention.
- type bandwidth_Hz:
float, optional, keyword-only
- param temperature_K:
Fibre temperature \(T\), which sets the phonon occupancy.
- type temperature_K:
float, optional, keyword-only
- param n_nodes:
Number of mesh points of the initial \(z\) grid.
- type n_nodes:
int, optional, keyword-only
- param tol:
Tolerance passed to the solver.
- type tol:
float, optional, keyword-only
- returns:
RamanSolution – Power profiles and the figures of merit read off them.
- raises ValueError:
If no pump is on, if a power or a length is negative, if a pump does not sit above every signal in frequency, if a group’s arguments disagree on the number of waves, if several waves need a
spectrum=and none was given, or if the boundary value problem fails to converge – an unconverged mesh is never returned, because it looks exactly like a plausible result.
References
G. P. Agrawal, Nonlinear Fiber Optics, 5th ed., Academic Press, 2013, Chapter 8.
M. N. Islam, “Raman amplifiers for telecommunications”, IEEE J. Sel. Topics Quantum Electron., vol. 8, no. 3, pp. 548-559, 2002.
Examples
>>> solution = solve_raman(length_km=80.0, gain_peak_W_km=0.4, ... pump_backward_W=0.2) >>> round(solution.on_off_gain_dB, 2), round(solution.net_gain_dB, 2) (5.97, -10.03)
Three C-band channels under a single pump: the gain follows the shape of the spectrum, so the comb comes out tilted.
>>> comb = [1530.0, 1545.0, 1560.0] >>> blow_wood = get_gain_spectrum("blow-wood") >>> one = solve_raman(length_km=80.0, gain_peak_W_km=0.4, ... wavelength_signal_nm=comb, signal_W=1e-3, ... pump_backward_W=0.4, wavelength_pump_nm=1450.0, ... spectrum=blow_wood, bandwidth_Hz=0.0) >>> print(np.round(one.on_off_gain_dB, 2)) [ 9.74 11.84 10.89]
The same total pump power split between two wavelengths halves the tilt – which is the whole reason multi-pump Raman exists:
>>> two = solve_raman(length_km=80.0, gain_peak_W_km=0.4, ... wavelength_signal_nm=comb, signal_W=1e-3, ... pump_backward_W=[0.15, 0.25], ... wavelength_pump_nm=[1425.0, 1455.0], ... spectrum=blow_wood, bandwidth_Hz=0.0) >>> two.n_signals, two.n_pumps (3, 2) >>> print(round(one.tilt_dB, 2), round(two.tilt_dB, 2)) 2.1 0.98
- get_gain_spectrum(standard: str, **kwargs: Any) RamanGainSpectrum[source]#
Return a catalog gain spectrum by name.
- Parameters:
standard (str) – Entry name; see
available_gain_spectra().**kwargs – Forwarded to the entry factory.
- Raises:
KeyError – If the name is not in the catalog.
Examples
>>> get_gain_spectrum("blow-wood").standard 'Blow-Wood single damped oscillator'
- register_gain_spectrum(name: str)[source]#
Register a catalog entry; users can add their own fits.
- Parameters:
name (str) – Name the factory answers to in
get_gain_spectrum().
Examples
>>> @register_gain_spectrum("LAB") ... def _lab(): ... return RamanGainSpectrum(triangular=14.0, standard="LAB", ... reference="bench") >>> round(get_gain_spectrum("LAB").peak_shift_THz, 1) 14.0
The registry is process-wide, so an example that adds to it has to take it back out: a doctest that leaves “LAB” behind makes every later test that iterates the catalog see an entry that only exists when the doctests ran first, which is a failure that depends on collection order.
>>> _ = _SPECTRUM_REGISTRY.pop("LAB") >>> "LAB" in available_gain_spectra() False
- available_gain_spectra() list[str][source]#
Names accepted by
get_gain_spectrum().- Returns:
list of str – Registered names, sorted.
Examples
>>> [name for name in available_gain_spectra() if "-" in name] ['blow-wood']