Devices#

class Laser(P_dBm: float = -10, *, linewidth: float = 0, theta0: float | None = None, seed: int | None = None, fs: float = 1000000000.0, freq_offset: float = 0, name: str = 'Laser')[source]#

Continuous-wave laser envelope generator with Wiener phase noise.

Signal Model#

The generated complex envelope is

\[E[n] = \sqrt{P_s} \, e^{j \left(2 \pi \Delta f \, n T_s + \theta_0 + \theta[n]\right)}, \qquad \theta[n] = \sum_{k=0}^{n} \delta\theta_k, \qquad \delta\theta_k \sim \mathcal{N}\left(0, \tfrac{2 \pi \Delta\nu}{f_s}\right)\]

where \(P_s = 10^{(P_{dBm} - 30)/10}\) is the emitted power in W, \(T_s = 1/f_s\) the sampling period, \(\Delta f\) the carrier frequency offset and \(\theta[n]\) a Wiener phase noise of linewidth \(\Delta\nu\).

Axes: generator – consumes a sample count nb_samples and produces a 1D complex envelope (N,).

param P_dBm:

Laser power \(P_{dBm}\) in dBm. Default is -10.

type P_dBm:

float

param linewidth:

Laser linewidth \(\Delta\nu\) in Hz. Default is 0 (no phase noise).

type linewidth:

float, keyword-only

param theta0:

Initial phase \(\theta_0\) in rad. When None, drawn uniformly on \([-\pi, \pi)\) from the local generator.

type theta0:

float, optional, keyword-only

param seed:

Local RNG seed (for \(\theta_0\) and the phase noise).

type seed:

int, optional, keyword-only

param fs:

Sampling frequency \(f_s\) in Hz. Default is 1e9.

type fs:

float, keyword-only

param freq_offset:

Carrier frequency offset \(\Delta f\) in Hz, relative to the nominal laser wavelength. Default is 0.

type freq_offset:

float, keyword-only

param name:

Name of the laser instance. Default is "Laser".

type name:

str, optional, keyword-only

References

T. Pfau, S. Hoffmann, R. Noe, “Hardware-Efficient Coherent Digital Receiver Concept With Feedforward Carrier Recovery for M-QAM Constellations,” Journal of Lightwave Technology, vol. 27, no. 8, pp. 989-999, 2009.

Examples

>>> laser = Laser(0, linewidth=0, theta0=0.0, fs=1e9)
>>> E = laser(3)
>>> print(np.round(np.abs(E) ** 2, 6))
[0.001 0.001 0.001]
class Optical90HybridCircuit(is_ideal: bool = True, *, sensitivity: float = 0.6, laser_in: Callable[[...], ndarray] | None = None, name: str = 'Optical90HybridCircuit')[source]#

Optical 90-degree hybrid front-end for coherent detection.

Signal Model#

In the ideal mode, the optic-electro conversion is transparent:

\[y[n] = x[n]\]

In the non-ideal mode, the incoming field is mixed with the local oscillator laser \(E_{lo}[n]\) and scaled by the receiver sensitivity \(\rho\):

\[y[n] = \rho \, x[n] \, E_{lo}^*[n]\]

where \(E_{lo}[n]\) is generated by laser_in (unit amplitude when no laser is provided).

Axes: element-wise – applied pointwise; one local-oscillator draw per array element, whatever the shape.

param is_ideal:

If True, ideal conversion (identity). If False, mixing with the local oscillator and sensitivity scaling. Default is True.

type is_ideal:

bool

param sensitivity:

Sensitivity factor \(\rho\) of the circuit (non-ideal mode). Default is 0.6.

type sensitivity:

float, keyword-only

param laser_in:

Local oscillator laser generating \(E_{lo}[n]\). Default is None (unit-amplitude local oscillator).

type laser_in:

Laser, optional, keyword-only

param name:

Name of the device instance. Default is "Optical90HybridCircuit".

type name:

str, optional, keyword-only

References

K. Kikuchi, “Fundamentals of coherent optical fiber communications,” Journal of Lightwave Technology, vol. 34, no. 1, pp. 157-179, 2016.

Examples

>>> x = np.array([1 + 1j, -1 + 0.5j])
>>> y = Optical90HybridCircuit()(x)
>>> bool(np.array_equal(y, x))
True
class PowerControl(P_moy: float = 1, *, unit: Literal['natural', 'dBm'] = 'natural', name: str = 'power_control')[source]#

Rescales a signal to a target power level.

Signal Model#

\[\begin{split}y[n] = g \, x[n], \qquad g = \begin{cases} P_{target} / \sqrt{P_x} & \text{(natural)} \\[4pt] \sqrt{10^{(P_{target} - 30)/10}} / \sqrt{P_x} & \text{(dBm)} \end{cases}\end{split}\]

where \(P_x = \mathbb{E}\left[|x[n]|^2\right]\) is the measured input power and \(P_{target}\) the target level. In "dBm" mode the output power is \(10^{(P_{target} - 30)/10}\) W; in "natural" mode the output RMS amplitude is \(P_{target}\).

Axes: element-wise – pointwise scaling; the gain \(g\) is estimated from the whole array.

param P_moy:

Target level \(P_{target}\); interpreted according to unit. Default is 1.

type P_moy:

float

param unit:

Unit of P_moy. Default is "natural".

type unit:

{“natural”, “dBm”}, keyword-only

param name:

Name of the device instance. Default is "power_control".

type name:

str, optional, keyword-only

Examples

>>> x = np.array([2.0, -2.0, 2.0, -2.0])
>>> y = PowerControl(1)(x)
>>> print(y)
[ 1. -1.  1. -1.]
class ErbiumDopedFiberAmplifier(gain: float, N_ase: float, *, name: str = 'ErbiumDopedFiberAmplifier', seed: int | None = None)[source]#

Erbium-doped fiber amplifier (EDFA): flat gain plus additive ASE noise.

Signal Model#

\[y[n] = \sqrt{G} \, x[n] + b[n], \qquad b[n] \sim \mathcal{CN}\left(0, N_{\mathrm{ase}}\right)\]

where \(\sqrt{G}\) is the amplitude gain of the amplifier and \(N_{\mathrm{ase}}\) the variance of the circular amplified spontaneous emission (ASE) noise, per state of polarization.

Axes: element-wise – pointwise gain plus additive noise; one independent complex ASE draw per array element, whatever the shape – a polarization pair (..., 2, N) gets independent noise per polarization.

param gain:

Amplitude gain \(\sqrt{G}\) (linear).

type gain:

float

param N_ase:

ASE noise variance \(N_{\mathrm{ase}}\) (in W), per state of polarization.

type N_ase:

float

param seed:

Local RNG seed.

type seed:

int, optional, keyword-only

param name:

Name of the amplifier instance. Default is "ErbiumDopedFiberAmplifier".

type name:

str, optional, keyword-only

References

Examples

>>> x = np.ones(3, dtype=complex)
>>> y = ErbiumDopedFiberAmplifier(2.0, 0.0)(x)
>>> print(y)
[2.+0.j 2.+0.j 2.+0.j]
class MachZehnderModulator(is_ideal: bool = False, *, Vpi: float = 6, k: float = 1, gI: float = 1, Phi: float = 1.5707963267948966, laser_in: Callable[[...], ndarray] | None = None, name: str = 'MachZehnderModulator')[source]#

IQ Mach-Zehnder modulator (MZM).

Signal Model#

In the ideal mode, the modulation is linear:

\[y[n] = -\frac{\pi V_{pp}}{4 V_\pi} \, E_{laser}[n] \, x[n]\]

In the non-ideal mode, each branch applies its cosine transfer function to the drive voltages:

\[y[n] = \left[ \cos\left(\frac{\pi \, u_I[n]}{2 V_\pi}\right) + e^{j \Phi} \cos\left(\frac{\pi \, u_Q[n]}{2 V_\pi}\right) \right] E_{laser}[n]\]
\[u_I[n] = V_\pi + \frac{g_I}{2} \frac{V_{pp}}{m} \, \Re(x[n]), \qquad u_Q[n] = V_\pi + \frac{g_Q}{2} \frac{V_{pp}}{m} \, \Im(x[n])\]

where \(V_\pi\) is the half-wave voltage, \(V_{pp} = k V_\pi\) the peak-to-peak drive voltage, \(g_I\) and \(g_Q = 2 - g_I\) the branch gains, \(\Phi\) the I/Q phase offset, \(m = \max(\max|\Re(x)|, \max|\Im(x)|)\) a normalization coefficient and \(E_{laser}[n]\) the input laser field (unit amplitude when no laser is provided).

Axes: element-wise – applied pointwise; the normalization \(m\) is estimated from the whole array (batch rows pooled) and one laser draw is made per array element.

param is_ideal:

If True, ideal linear modulation without imbalance. Default is False.

type is_ideal:

bool

param Vpi:

Half-wave voltage \(V_\pi\) in V. Default is 6.

type Vpi:

float, keyword-only

param k:

Peak-to-peak drive ratio \(k = V_{pp} / V_\pi\). Default is 1.

type k:

float, keyword-only

param gI:

Gain \(g_I\) of the I branch (the Q branch gets \(g_Q = 2 - g_I\)). Default is 1.

type gI:

float, keyword-only

param Phi:

Phase offset \(\Phi\) between the I and Q branches in rad. Default is np.pi / 2.

type Phi:

float, keyword-only

param laser_in:

Input laser field generator producing \(E_{laser}[n]\). Default is None (unit-amplitude field).

type laser_in:

callable, optional, keyword-only

param name:

Name of the modulator instance. Default is "MachZehnderModulator".

type name:

str, optional, keyword-only

References

C. Peucheret, “Generation and Detection of Optical Modulation Formats,” Technical University of Denmark, 2012.

Examples

>>> mzm = MachZehnderModulator(is_ideal=True, Vpi=6.0, k=1.0)
>>> y = mzm(np.array([1.0 + 0j]))
>>> print(round(float(np.real(y[0])), 4))
-0.7854