|
| 1 | +"""Photodetectors. |
| 2 | +
|
| 3 | +Model reference: G. P. Agrawal, *Fiber-Optic Communication Systems*, ch. 4 |
| 4 | +(photodetectors, receiver noise). |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import numpy as np |
| 10 | + |
| 11 | +from ..component import BoolParam, Component, Param, PortType |
| 12 | +from ..context import SimulationContext |
| 13 | +from ..signals import ElectricalSignal, OpticalSignal, Signal |
| 14 | +from ..units import K_BOLTZMANN, Q_ELECTRON |
| 15 | + |
| 16 | + |
| 17 | +class PINPhotodiode(Component): |
| 18 | + """PIN photodiode: square-law detection with shot and thermal noise. |
| 19 | +
|
| 20 | + The mean photocurrent is ``I = R * P + I_dark``, with ``R`` the responsivity |
| 21 | + [A/W]. Two noise sources are added, both white over the simulated bandwidth |
| 22 | + ``B = fs / 2``: |
| 23 | +
|
| 24 | + * **Shot noise**, variance ``2 * q * I * B``. It scales with the |
| 25 | + instantaneous current, so it is generated per sample rather than as a |
| 26 | + single constant — bright samples really are noisier than dark ones, which |
| 27 | + is why an eye diagram's rails have different thicknesses. |
| 28 | + * **Thermal (Johnson) noise**, variance ``4 * k * T * B / R_load``, |
| 29 | + independent of the received power. |
| 30 | +
|
| 31 | + Two simplifications worth stating plainly, both of which change results and |
| 32 | + neither of which is hidden by the interface: |
| 33 | +
|
| 34 | + * Bands are detected incoherently — powers add. Beating between bands lands |
| 35 | + at their frequency separation, far above any realistic receiver bandwidth |
| 36 | + for the channel spacings this is used with, but it is genuinely absent |
| 37 | + rather than merely negligible. |
| 38 | + * Noise bins contribute mean power and its shot noise, but signal-ASE beat |
| 39 | + noise is not modelled. That term only matters once there is an amplifier |
| 40 | + to produce ASE, and it arrives with the EDFA in Phase 1.5. |
| 41 | + """ |
| 42 | + |
| 43 | + display_name = "PIN Photodiode" |
| 44 | + category = "Receivers" |
| 45 | + |
| 46 | + responsivity = Param(0.8, unit="", min=0.0, doc="Responsivity R [A/W]") |
| 47 | + dark_current = Param(0.0, unit="", min=0.0, doc="Dark current [A]") |
| 48 | + load_resistance = Param(50.0, unit="", min=0.0, doc="Load resistance [ohm]") |
| 49 | + temperature = Param(300.0, unit="", min=0.0, doc="Receiver temperature [K]") |
| 50 | + shot_noise = BoolParam(True, doc="Add shot noise") |
| 51 | + thermal_noise = BoolParam(True, doc="Add thermal (Johnson) noise") |
| 52 | + |
| 53 | + inputs = {"in": PortType.OPTICAL} |
| 54 | + outputs = {"out": PortType.ELECTRICAL} |
| 55 | + |
| 56 | + def noise_bandwidth(self, ctx: SimulationContext) -> float: |
| 57 | + """Effective one-sided noise bandwidth [Hz] of the sampled representation.""" |
| 58 | + return ctx.sample_rate / 2.0 |
| 59 | + |
| 60 | + def run(self, ctx: SimulationContext, inputs: dict[str, Signal]) -> dict[str, Signal]: |
| 61 | + signal: OpticalSignal = inputs["in"] |
| 62 | + |
| 63 | + power = np.zeros(ctx.num_samples, dtype=np.float64) |
| 64 | + for band in signal.bands: |
| 65 | + power += np.abs(band.Ex.astype(np.complex128)) ** 2 |
| 66 | + power += np.abs(band.Ey.astype(np.complex128)) ** 2 |
| 67 | + power += signal.noise_power() |
| 68 | + |
| 69 | + current = self.si("responsivity") * power + self.si("dark_current") |
| 70 | + bandwidth = self.noise_bandwidth(ctx) |
| 71 | + |
| 72 | + if self.shot_noise: |
| 73 | + # Variance tracks the instantaneous current, so it is per sample: |
| 74 | + # bright samples really are noisier than dark ones. |
| 75 | + shot_variance = 2.0 * Q_ELECTRON * np.maximum(current, 0.0) * bandwidth |
| 76 | + rng = ctx.rng("PINPhotodiode", self.label, "shot") |
| 77 | + current = current + rng.normal(0.0, np.sqrt(shot_variance)) |
| 78 | + |
| 79 | + if self.thermal_noise and self.si("load_resistance") > 0.0: |
| 80 | + thermal_variance = ( |
| 81 | + 4.0 * K_BOLTZMANN * self.si("temperature") * bandwidth / self.si("load_resistance") |
| 82 | + ) |
| 83 | + rng = ctx.rng("PINPhotodiode", self.label, "thermal") |
| 84 | + current = current + rng.normal(0.0, np.sqrt(thermal_variance), size=ctx.num_samples) |
| 85 | + |
| 86 | + return { |
| 87 | + "out": ElectricalSignal( |
| 88 | + samples=current.astype(ctx.real_dtype), fs=ctx.sample_rate, unit="A" |
| 89 | + ) |
| 90 | + } |
0 commit comments