Electro-Absorption Modulator

An electro-absorption modulator (EAM) is a short waveguide section made of a semiconductor whose absorption edge lies just below the photon energy of the light. A reverse bias across a p-i-n junction puts an electric field on the absorbing core, and the field moves the absorption edge to longer wavelengths. The light is absorbed more strongly as the reverse bias grows, so the transmitted power follows the drive voltage directly. In bulk semiconductors such as germanium the mechanism is the Franz-Keldysh effect [2]; in quantum wells it is the quantum-confined Stark effect. Germanium EAMs on silicon are only tens of micrometres long, have junction capacitances of a few femtofarads, and need no traveling-wave electrode, which makes them compact, fast and efficient intensity modulators for short-reach links [1, 3].
ElectroAbsorptionModTimeStepper
models the modulator in the time domain: the bias-dependent absorption, a bias-independent insertion
loss, the chirp of the modulated light, the RC low-pass of the junction, and the photocurrent
generated by the absorbed light. The absorption is supplied as a table over voltage and wavelength,
which franz_keldysh_absorption can
generate for a bulk absorber.
electro_absorption_modulator
wraps it in a component with two optical ports, P0 and P1, and one electrical port, E0. The
same component also carries an
AnalyticWaveguideModel with the same table
for frequency-domain simulations at a fixed bias.
This guide shows how to use them for a germanium EAM on silicon, with the device parameters of Liu et al. [1]: building and calibrating the absorption table, the transmission spectra, the transfer curve against bias, the photocurrent, the modulation bandwidth, the eye diagram of non-return-to-zero (NRZ) data, and the chirp.
References
- Liu, Y., Sun, J., Song, R., et al. “80Gb/s NRZ Ge waveguide electro-absorption modulator.” Optics Express 2022 30 (19), 34276-34286, doi: 10.1364/OE.462890.
- Chuang, S. L. Physics of Photonic Devices, 2nd ed. Wiley 2009.
- Liu, J., Beals, M., Pomerene, A., et al. “Waveguide-integrated, ultralow-energy GeSi electro-absorption modulators.” Nature Photonics 2008 2 (7), 433-437, doi: 10.1038/nphoton.2008.99.
- Koyama, F., and Iga, K. “Frequency chirping in external modulators.” Journal of Lightwave Technology 1988 6 (1), 87-93, doi: 10.1109/50.3969.
What the time stepper computes
Section titled “What the time stepper computes”Let be the absorption coefficient of the material at the applied voltage and the wavelength . Only the fraction of the optical mode that overlaps the absorbing core is absorbed, so the power decays along the section as . Over the length the power transmission is , and the field is multiplied by the square root of this factor.
A change of absorption always comes with a change of refractive index, so the phase of the light follows its intensity. The chirp parameter of the modulator, as defined by Koyama and Iga [4] and quoted on datasheets, relates the instantaneous frequency deviation of the output to its power:
PhotonForge uses the time convention, in which an envelope is light at , so the frequency deviation is for an envelope phase . The chirp therefore appears as the phase , and with a bias-independent loss in dB the output field is
with . The absorption table is evaluated once, at the carrier wavelength, because the absorption edge varies over terahertz while a data signal occupies tens of gigahertz.
The voltage is the sum of a constant bias and the drive read from the electrical port. A signal of amplitude on that port becomes a voltage through the port impedance , . Before it reaches the absorber, the drive passes through a first-order low-pass with the cutoff , which represents the charging of the junction capacitance through the series resistance , .
Every absorbed photon can generate an electron-hole pair, which the junction field collects. With the quantum efficiency , the photon energy and the elementary charge , the photocurrent is
and it is emitted on the electrical port as the amplitude .
The component’s parameters map onto these quantities as follows:
absorptionis the table in 1/μm, as an Interpolator over voltage and wavelength.lengthis in μm, andconfinement_factoris .biasis the constant bias in V. Reverse bias is negative.residual_lossis in dB, for example the loss of the transitions into and out of the absorbing section.chirpis .f_3dBis the cutoff of the junction low-pass in Hz.Nonedisables the filter.quantum_efficiencyis . A value of 0 disables the photocurrent.n_groupis the group index for the optical transit delay through the section. The default, 0, disables the delay.
The device of Liu et al. [1] is a germanium waveguide 600 nm wide, 350 nm tall and 40 μm long, grown in a recess of a 220 nm silicon-on-insulator waveguide, with a lateral p-i-n junction across the germanium. The measured values used below are:
- Insertion loss at 1603 nm: 5.73 dB, of which 1.06 dB is the loss of the two transitions between the silicon and the germanium waveguides.
- Static extinction ratio between 0 V and -3 V at 1603 nm: 14.15 dB.
- Series resistance 250 Ω and junction capacitance 4.8 fF, from a fit to the measured electrical reflection.
- Responsivity as a photodetector at -3 V and 1610 nm: 1.04 A/W.
- Drive for the eye diagrams: 2.3 V peak to peak around a -3 V bias.
The confinement factor is not reported. A mode solve of the germanium core in its silicon recess places 85% of the mode intensity in the germanium, and we use .
import matplotlib.pyplot as pltimport numpy as npimport photonforge as pfimport photonforge.abstract as pfa
# Open the live viewer to display the modulator componentviewer = pf.live_viewer.LiveViewer()
# Use the basic technology for the abstract componentspf.config.default_technology = pf.basic_technology()
# Set the physical constants for the photon energy and the photocurrenth_planck = 6.62607015e-34 # Planck constant (J s)q = 1.602176634e-19 # Elementary charge (C)
# Set the operating wavelength and the carrier frequency (pf.C_0 is in um/s)wavelength = 1.603 # Operating wavelength (um)f0 = pf.C_0 / wavelength # Carrier frequency (Hz)
# Set the geometry and the measured losses of the modulatorlength = 40.0 # Length of the germanium section (um)confinement = 0.85 # Confinement factor of the mode in the germaniuminsertion_loss = 5.73 # Measured insertion loss at 0 V (dB)transition_loss = 1.06 # Loss of the two silicon-germanium transitions (dB)
# Set the electrical parameters of the junctionr_s = 250.0 # Series resistance (Ohm)c_j = 4.8e-15 # Junction capacitance (F)z0 = 50.0 # Reference impedance of the electrical port (Ohm)
# Set the operating point of the drivebias = -3.0 # Reverse bias of the junction (V)v_pp = 2.3 # Peak-to-peak drive voltage (V)
# Compute the RC cutoff of the junction, 1 / (2 pi R_s C_j)f_rc = 1 / (2 * np.pi * r_s * c_j)print(f"RC cutoff frequency: {f_rc / 1e9:.1f} GHz")LiveViewer started at http://127.0.0.1:42949. Call its 'stop()' method to free the port.RC cutoff frequency: 132.6 GHzAbsorption table from the Franz-Keldysh effect
Section titled “Absorption table from the Franz-Keldysh effect”The electric field across the intrinsic region tilts the bands of the semiconductor. An electron can then absorb a photon with an energy below the bandgap and tunnel the rest of the way into the conduction band. The absorption edge acquires a tail below the bandgap that grows with the field, which is the Franz-Keldysh effect. With the reduced effective mass of the electron and the light hole, the field sets the energy scale
and the absorption at the photon energy below the bandgap energy is [2]
where is the Airy function and a scale constant. The field follows from the voltage across the intrinsic width and the built-in potential of the junction, , so a negative (reverse) bias increases the field.
pf.franz_keldysh_absorption evaluates this expression on a grid of wavelengths and voltages and
returns the table as an interpolator. Only the dependence on photon energy and field is physical;
the absolute scale , the absorption_scale argument, is calibrated to one measured
value. We calibrate it to the measured insertion loss at 0 V. Without the transitions, the
germanium section loses dB, and a loss in dB corresponds to
The table uses an intrinsic width of 0.4 μm, the 600 nm germanium core less its implanted edges, and the default built-in potential of 0.7 V and reduced mass of 0.02 electron masses. The bandgap wavelength is set to 1.582 μm, which makes the table reproduce the measured static extinction ratio. It corresponds to 0.784 eV, just below the 0.80 eV direct gap of bulk germanium, since the tensile strain of growth on silicon lowers the gap.
# Set the grid of the absorption tablewavelengths = np.linspace(1.55, 1.65, 101) # Wavelength grid (um)voltages = np.linspace(-5.0, 0.0, 51) # Voltage grid (V)
# Set the Franz-Keldysh parameters of the germanium junctionfk_parameters = { "wavelengths": wavelengths, "voltages": voltages, "bandgap_wavelength": 1.582, # Direct bandgap wavelength (um) "intrinsic_thickness": 0.4, # Width of the intrinsic region (um)}
# Build the table with the default absorption scale of 1absorption_raw = pf.franz_keldysh_absorption(**fk_parameters)
# Convert the measured 0 V loss of the germanium section to an absorption (1/um)db_per_neper = 10 * np.log10(np.e) # Loss in dB of a power decay by eloss_0v = insertion_loss - transition_loss # Loss of the germanium section at 0 V (dB)alpha_0v = loss_0v / (db_per_neper * confinement * length)
# Scale the table so that it returns alpha_0v at 0 V and the operating wavelengthalpha_raw = absorption_raw(np.array([[0.0, wavelength]]))[0]absorption = pf.franz_keldysh_absorption( **fk_parameters, absorption_scale=alpha_0v / alpha_raw)
# Evaluate the calibrated table at 0 V and at the bias, at the operating wavelengthalpha_bias = absorption(np.array([[bias, wavelength]]))[0]print(f"Absorption at 0 V: {alpha_0v * 1e4:.0f} 1/cm")print(f"Absorption at {bias:g} V: {alpha_bias * 1e4:.0f} 1/cm")
# Compute the static extinction ratio between 0 V and the bias (dB)extinction_static = db_per_neper * confinement * length * (alpha_bias - alpha_0v)print(f"Static extinction ratio, 0 V to {bias:g} V: {extinction_static:.2f} dB")
# Plot the absorption spectrum at several voltagesfig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)for v in [0.0, -1.0, -2.0, -3.0, -4.0]: # Evaluate the table along the wavelength axis at this voltage points = np.column_stack([np.full(wavelengths.size, v), wavelengths]) ax.semilogy(wavelengths * 1e3, absorption(points) * 1e4, label=f"{v:g} V")
# Mark the operating wavelengthax.axvline(wavelength * 1e3, color="gray", lw=0.8)ax.set(xlabel="Wavelength (nm)", ylabel="Absorption (1/cm)", ylim=(10, 2e4))ax.legend()plt.show()Absorption at 0 V: 316 1/cmAbsorption at -3 V: 1264 1/cmStatic extinction ratio, 0 V to -3 V: 13.99 dB
At the operating wavelength the absorption rises from 316 1/cm at 0 V to 1264 1/cm at -3 V, a factor of four, and the calibrated table gives a static extinction ratio of 13.99 dB between the two biases. The curves show how the Franz-Keldysh effect shapes the absorption:
- At wavelengths longer than the bandgap wavelength, the field raises the absorption tail. The relative change grows with wavelength, while the absorption itself falls.
- Below about 1565 nm, above the bandgap, the field lowers the absorption slightly. An EAM is therefore operated on the long-wavelength side of the edge.
Creating the modulator
Section titled “Creating the modulator”pfa.electro_absorption_modulator builds the component from the table and the device parameters.
The junction low-pass is set to the RC cutoff computed above, and the transitions enter as the
bias-independent residual_loss.
# Create the modulator at the operating biaseam = pfa.electro_absorption_modulator( absorption=absorption, length=length, confinement_factor=confinement, bias=bias, residual_loss=transition_loss, f_3dB=f_rc, z0=z0,)viewer(eam)Transmission spectra
Section titled “Transmission spectra”The frequency-domain model of the component evaluates the same table at the fixed bias, over any
range of wavelengths. Two figures describe the static performance of an EAM at each wavelength:
- the insertion loss (IL), the loss at 0 V, including the transitions;
- the extinction ratio (ER), the ratio of the transmission at 0 V to the transmission at the reverse bias.
The next cell computes the S matrix of the component at biases from 0 V to -3 V and plots the transmission spectra, the insertion loss, and the extinction ratio at -3 V.
# Set the wavelengths of the spectra and the corresponding frequenciesspectrum_wavelengths = np.linspace(1.56, 1.64, 161) # Wavelengths (um)frequencies = pf.C_0 / spectrum_wavelengths # Frequencies (Hz)
# Compute the transmission spectrum at each bias from the S matrixtransmission_db = {}for v in [0.0, -1.0, -2.0, -3.0]: # Create the modulator at this bias and compute its S matrix component = pfa.electro_absorption_modulator( absorption=absorption, length=length, confinement_factor=confinement, bias=v, residual_loss=transition_loss, ) s_matrix = component.s_matrix(frequencies, show_progress=False)
# Convert the transmission from P0 to P1 to dB transmission_db[v] = 20 * np.log10(np.abs(s_matrix["P0@0", "P1@0"]))
# Compute the insertion loss and the extinction ratio at -3 Vil = -transmission_db[0.0]er = transmission_db[0.0] - transmission_db[-3.0]
# Plot the transmission spectra in the left panelfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), tight_layout=True)for v, t_db in transmission_db.items(): ax1.plot(spectrum_wavelengths * 1e3, t_db, label=f"{v:g} V")ax1.set(xlabel="Wavelength (nm)", ylabel="Transmission (dB)")ax1.legend(fontsize=7)
# Plot the insertion loss and the extinction ratio in the right panelax2.plot(spectrum_wavelengths * 1e3, il, label="Insertion loss")ax2.plot(spectrum_wavelengths * 1e3, er, label="Extinction ratio at -3 V")ax2.axvline(wavelength * 1e3, color="gray", lw=0.8)ax2.set(xlabel="Wavelength (nm)", ylabel="Loss or ratio (dB)", ylim=(0, 30))ax2.legend(fontsize=7)plt.show()
# Print both figures at the operating wavelengthi_op = np.argmin(np.abs(spectrum_wavelengths - wavelength))print(f"At {wavelength * 1e3:.0f} nm: IL {il[i_op]:.2f} dB, ER {er[i_op]:.2f} dB")
# Find the wavelength of the largest extinction ratioi_max = np.argmax(er)print(f"Largest ER: {er[i_max]:.2f} dB at {spectrum_wavelengths[i_max] * 1e3:.0f} nm")
At 1603 nm: IL 5.73 dB, ER 13.99 dBLargest ER: 14.05 dB at 1600 nmThe insertion loss falls steeply with wavelength, from 28 dB at 1570 nm to 1.4 dB at 1640 nm, where the transitions account for most of it. The extinction ratio at -3 V is largest, 14.05 dB, near 1600 nm and falls slowly on either side. The operating wavelength of 1603 nm sits at this maximum, with 5.73 dB of insertion loss. A longer wavelength trades extinction ratio for lower loss: at 1620 nm the insertion loss is 2.6 dB and the extinction ratio 12.3 dB.
Transfer curve and operating point
Section titled “Transfer curve and operating point”In the time domain the modulator is driven through its electrical port. A single component needs
no surrounding circuit: setup_time_stepper returns its time stepper directly, and the inputs are
passed to step as a TimeSeries keyed by port name:
the optical field on "P0@0" and the drive on "E0@0". The optical fields are in
√W, so is the power in watts, and a drive voltage
is passed as the amplitude .
The next cell ramps the drive slowly from -2 V to +3 V around the -3 V bias, so the applied voltage sweeps from -5 V to 0 V in a single run, and plots the transmission against the applied voltage. The ramp lasts 2 ns, far longer than the 1.2 ps time constant of the junction, so the transmission follows the static transfer curve. The dotted line is the transmission computed directly from the table, in dB of loss, for reference. The shaded band marks the 2.3 V drive swing around the bias.
The junction low-pass starts every run from 0 V, so the applied voltage takes a few time constants to reach the bias. The first 10 ps of the run are discarded for this reason.
time_step = 0.25e-12 # Simulation time step (s)num_steps = 8000 # Number of time steps, a 2 ns rampp_in = 1e-3 # Input optical power (W)settle = 40 # Time steps discarded while the junction charges to the bias
# Build the CW optical input and the slow voltage ramp of the drivea_in = np.full(num_steps, np.sqrt(p_in), dtype=complex) # Optical field (sqrt(W))v_drive = np.linspace(-2.0, 3.0, num_steps) # Drive voltage (V)
# Set up the time stepper of the modulatortime_stepper = eam.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False)
# Run the ramp, passing the drive as the amplitude V / sqrt(Z0)inputs = pf.TimeSeries({"P0@0": a_in, "E0@0": v_drive / np.sqrt(z0) + 0j}, time_step)outputs = time_stepper.step(inputs=inputs, show_progress=False)
# Compute the transmission against the applied voltage, bias plus drivev_applied = (bias + v_drive)[settle:]transmission = np.abs(outputs["P1@0"][settle:]) ** 2 / p_in
# Compute the transmission from the table at the operating wavelengthpoints = np.column_stack([voltages, np.full(voltages.size, wavelength)])loss_table = transition_loss + db_per_neper * confinement * length * absorption(points)
# Set the two drive levels: the one level at low reverse bias, the zero level at highv_one = bias + v_pp / 2v_zero = bias - v_pp / 2
# Plot the transfer curve with the drive swingfig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)ax.plot(v_applied, 10 * np.log10(transmission), label="Time stepper")ax.plot(voltages, -loss_table, "k:", label="Absorption table")ax.axvspan(v_zero, v_one, color="gray", alpha=0.2)ax.set(xlabel="Applied voltage (V)", ylabel="Transmission (dB)")ax.legend()plt.show()
# Read the transmission at the two drive levels from the rampt_one = np.interp(v_one, v_applied, transmission)t_zero = np.interp(v_zero, v_applied, transmission)print(f"Transmission at {v_one:.2f} V (one level): {10 * np.log10(t_one):.2f} dB")print(f"Transmission at {v_zero:.2f} V (zero level): {10 * np.log10(t_zero):.2f} dB")print(f"Dynamic extinction ratio: {10 * np.log10(t_one / t_zero):.2f} dB")
Transmission at -1.85 V (one level): -15.86 dBTransmission at -4.15 V (zero level): -22.81 dBDynamic extinction ratio: 6.95 dBThe time stepper traces the same curve as the table, since at every step it evaluates the table at the applied voltage. The transmission falls from -5.73 dB at 0 V to -24.7 dB at -5 V, and the curve is steepest near 0 V. Around the -3 V bias, the 2.3 V swing gives a dynamic extinction ratio of 6.95 dB, and the one level at -1.85 V lies 15.86 dB below the input power.
The same ramp gives the two levels for any bias. The next cell moves the 2.3 V swing along the transfer curve and plots the dynamic extinction ratio and the loss of the one level against the bias.
# Sweep the bias, keeping both drive levels inside the rampbias_sweep = np.linspace(v_applied[0] + v_pp / 2, v_applied[-1] - v_pp / 2, 31)
# Read the transmission of both levels from the ramp (dB)t_one_db = 10 * np.log10(np.interp(bias_sweep + v_pp / 2, v_applied, transmission))t_zero_db = 10 * np.log10(np.interp(bias_sweep - v_pp / 2, v_applied, transmission))
# Plot the dynamic extinction ratio and the loss of the one level against the biasfig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)ax.plot(bias_sweep, t_one_db - t_zero_db, label="Dynamic extinction ratio")ax.plot(bias_sweep, -t_one_db, label="Loss of the one level")ax.axvline(bias, color="gray", lw=0.8)ax.set(xlabel="Bias (V)", ylabel="Loss or ratio (dB)")ax.legend()plt.show()
Moving the bias toward 0 V improves both figures, because the transfer curve is steepest there. At -1.15 V, where the one level reaches 0 V, the dynamic extinction ratio is 11.7 dB and the one level loses 5.8 dB, against 6.95 dB and 15.9 dB at -3 V.
Practical devices are biased deeper than this curve alone suggests. A strong field sweeps the photogenerated carriers out of the germanium quickly, which avoids absorption saturation at high optical power, and it fully depletes the intrinsic region, which keeps the junction capacitance at its minimum. Neither effect is part of the absorption table, so the bias is set from measurements of the device. The rest of this guide keeps the -3 V bias of [1].
Photocurrent
Section titled “Photocurrent”The light absorbed in the germanium is not lost: the junction collects the carriers, and the modulator also works as a photodetector. The photocurrent is emitted on the electrical port as the amplitude , so it can load the driver in a circuit, and it can be read to monitor the optical power or the bias point.
The quantum efficiency is calibrated to the measured responsivity. Operated as a photodetector at -3 V and 1610 nm, the device of Liu et al. gives A/W [1]. The responsivity of the model is , so
This value of also absorbs the input transition, since the model refers the photocurrent to the power at the input port. The next cell computes , recreates the modulator with it, and repeats the voltage ramp at 1 mW input power to plot the photocurrent against the applied voltage.
responsivity_measured = 1.04 # Measured responsivity at -3 V and 1610 nm (A/W)wavelength_pd = 1.61 # Wavelength of the responsivity measurement (um)
# Compute the fraction of the light absorbed at -3 V and 1610 nmalpha_pd = absorption(np.array([[-3.0, wavelength_pd]]))[0]absorbed_pd = 1 - np.exp(-confinement * alpha_pd * length)
# Compute the quantum efficiency from the ideal responsivity q / (h nu)ideal_responsivity = q / (h_planck * pf.C_0 / wavelength_pd)eta = responsivity_measured / (ideal_responsivity * absorbed_pd)print(f"Absorbed fraction at -3 V and 1610 nm: {absorbed_pd:.3f}")print(f"Quantum efficiency: {eta:.3f}")
# Recreate the modulator with the calibrated quantum efficiencyeam = pfa.electro_absorption_modulator( absorption=absorption, length=length, confinement_factor=confinement, bias=bias, residual_loss=transition_loss, f_3dB=f_rc, quantum_efficiency=eta, z0=z0,)
# Repeat the voltage ramp with the new modulatortime_stepper = eam.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False)outputs = time_stepper.step(inputs=inputs, show_progress=False)
# Convert the electrical output amplitude to the photocurrent, I = -Re(A) / sqrt(Z0)i_ph = -outputs["E0@0"][settle:].real / np.sqrt(z0)
# Compute the responsivity at the operating wavelength from the tableresponsivity_table = ( eta * q / (h_planck * f0) * (1 - np.exp(-confinement * absorption(points) * length)))
# Plot the photocurrent against the applied voltagefig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)ax.plot(v_applied, i_ph * 1e3, label="Time stepper")ax.plot(voltages, responsivity_table * p_in * 1e3, "k:", label="Absorption table")ax.axvspan(v_zero, v_one, color="gray", alpha=0.2)ax.set(xlabel="Applied voltage (V)", ylabel="Photocurrent (mA)")ax.legend()plt.show()
# Print the photocurrent at the two drive levelsfor level, v in [("one", v_one), ("zero", v_zero)]: print( f"Photocurrent at {v:.2f} V ({level} level): " f"{np.interp(v, v_applied, i_ph) * 1e3:.3f} mA" )Absorbed fraction at -3 V and 1610 nm: 0.978Quantum efficiency: 0.819
Photocurrent at -1.85 V (one level): 1.024 mAPhotocurrent at -4.15 V (zero level): 1.052 mAThe quantum efficiency comes out at 0.82: 82% of the photons absorbed in the germanium, counted at the input port, are collected as current. The rest are lost in the input transition and to recombination.
The photocurrent follows the absorbed fraction. It rises from 0.71 mA at 0 V and levels off near 1.05 mA, where almost all of the light is absorbed. Across the drive swing it changes by only 3%, from 1.024 mA at the one level to 1.052 mA at the zero level. The photocurrent flows against the bias, so with 1 mW of input power the modulator dissipates about 3.1 mW at -3 V. This heat, and the current drawn from the bias supply, grow in proportion to the optical power.
Modulation bandwidth
Section titled “Modulation bandwidth”The Franz-Keldysh effect responds in well under a picosecond, so the speed of the modulator is set by the RC charging of the junction [1]. The time stepper applies this as a first-order low-pass on the drive, with the response
The electro-optic (EO) response is measured the way a lightwave component analyzer measures it: a small sinusoidal drive at the frequency is added to the bias, and the amplitude of the resulting modulation of the output power is recorded. The next cell sweeps from 1 GHz to 300 GHz with a 20 mV amplitude, small enough for the transfer curve to be linear. Each run lasts 40 periods, and the amplitude is fitted over the last 20, after the filter has settled. The response is normalized to its value at the lowest frequency and shown with the RC law for reference.
v_tone = 0.02 # Amplitude of the small-signal drive (V)num_periods = 40 # Number of drive periods in each run
# Sweep the modulation frequency (Hz)f_mod = np.geomspace(1e9, 300e9, 25)
response = []for f in f_mod: # Build the sinusoidal drive for this frequency t = np.arange(int(round(num_periods / f / time_step))) * time_step v_drive = v_tone * np.sin(2 * np.pi * f * t)
# Run the modulator on the CW input and the drive time_stepper = eam.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) tone_inputs = pf.TimeSeries( { "P0@0": np.full(t.size, np.sqrt(p_in), dtype=complex), "E0@0": v_drive / np.sqrt(z0) + 0j, }, time_step, ) p_out = ( np.abs(time_stepper.step(inputs=tone_inputs, show_progress=False)["P1@0"]) ** 2 )
# Fit a constant plus a sinusoid at f to the second half of the output power half = t.size // 2 basis = np.column_stack( [ np.ones(t.size - half), np.cos(2 * np.pi * f * t[half:]), np.sin(2 * np.pi * f * t[half:]), ] ) coefficients = np.linalg.lstsq(basis, p_out[half:], rcond=None)[0] response.append(np.hypot(coefficients[1], coefficients[2]))
# Normalize the response to the lowest frequency and convert to dBresponse_db = 20 * np.log10(np.array(response) / response[0])
# Compute the RC law, normalized in the same wayrc_law_db = -10 * np.log10((1 + (f_mod / f_rc) ** 2) / (1 + (f_mod[0] / f_rc) ** 2))
# Plot the EO response with the RC lawfig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)ax.semilogx(f_mod / 1e9, response_db, "o", label="Time stepper")ax.semilogx(f_mod / 1e9, rc_law_db, "k:", label="RC law")ax.axhline(-3, color="gray", lw=0.8)ax.set(xlabel="Modulation frequency (GHz)", ylabel="EO response (dB)")ax.legend()plt.show()
# Interpolate the frequency where the response crosses -3 dBf_3db_eo = np.interp(3, -response_db, f_mod)print(f"EO 3 dB bandwidth: {f_3db_eo / 1e9:.0f} GHz")
EO 3 dB bandwidth: 131 GHzThe response follows the RC law and crosses -3 dB at 131 GHz, at the RC cutoff of the junction. For comparison, [1] measured an EO bandwidth beyond 67 GHz, the upper limit of the instrument used.
Eye diagram of NRZ data
Section titled “Eye diagram of NRZ data”The next cell drives the modulator with random NRZ data at 2.3 V peak to peak around the -3 V bias, at 80 Gb/s, the highest rate measured in [1], and at 160 Gb/s. A one bit is the level of low reverse bias, V, and a zero bit the level of high reverse bias, -4.15 V. The edges of the drive are smoothed with a Gaussian filter to represent the finite bandwidth of the driver. The output power is folded into two-bit segments to form the eye, and the extinction ratio is measured from the average power at the bit centers. The first two bits are discarded while the junction charges to the bias.
num_bits = 256 # Number of random bitsnum_skip = 2 # Bits discarded while the junction charges to the biasedge_fraction = 0.15 # Width of the edge-smoothing Gaussian, as a fraction of a bit
# Draw a reproducible random bit sequencerng = np.random.default_rng(1)bits = rng.integers(0, 2, num_bits)
fig, axes = plt.subplots(1, 2, figsize=(7, 3), sharey=True, tight_layout=True)for ax, bit_rate in zip(axes, [80e9, 160e9]): # Compute the number of samples per bit, the unit interval unit_interval = int(round(1 / bit_rate / time_step))
# Map the bits to drive voltages and hold each for one bit v_drive = np.repeat(np.where(bits == 1, v_pp / 2, -v_pp / 2), unit_interval)
# Smooth the edges of the drive with a Gaussian filter width = edge_fraction * unit_interval k = np.arange(-4 * width, 4 * width + 1) kernel = np.exp(-0.5 * (k / width) ** 2) v_drive = np.convolve( np.pad(v_drive, k.size, mode="edge"), kernel / kernel.sum(), "same" ) v_drive = v_drive[k.size : -k.size]
# Run the modulator on the CW input and the data drive time_stepper = eam.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) data_inputs = pf.TimeSeries( { "P0@0": np.full(v_drive.size, np.sqrt(p_in), dtype=complex), "E0@0": v_drive / np.sqrt(z0) + 0j, }, time_step, ) p_out = ( np.abs(time_stepper.step(inputs=data_inputs, show_progress=False)["P1@0"]) ** 2 )
# Fold the output into two-bit segments, centered on the bits, to form the eye segments = p_out[num_skip * unit_interval + unit_interval // 2 :] segments = segments[: len(segments) // (2 * unit_interval) * 2 * unit_interval] t_eye = np.arange(2 * unit_interval) / unit_interval for segment in segments.reshape(-1, 2 * unit_interval): ax.plot(t_eye, segment * 1e6, "C0", alpha=0.15, lw=0.8) ax.set(title=f"{bit_rate / 1e9:g} Gb/s", xlabel="Time (bits)")
# Measure the extinction ratio from the average power at the bit centers centers = p_out[np.arange(num_skip, num_bits) * unit_interval + unit_interval // 2] ones = centers[bits[num_skip:] == 1] zeros = centers[bits[num_skip:] == 0] ratio_db = 10 * np.log10(ones.mean() / zeros.mean()) print(f"{bit_rate / 1e9:g} Gb/s: dynamic extinction ratio {ratio_db:.2f} dB")
axes[0].set_ylabel("Output power (µW)")plt.show()80 Gb/s: dynamic extinction ratio 6.83 dB160 Gb/s: dynamic extinction ratio 6.25 dB
At 80 Gb/s a bit lasts 12.5 ps, ten time constants of the junction, and the eye is wide open. The
dynamic extinction ratio is 6.83 dB, close to the 6.95 dB of the static levels. At 160 Gb/s a bit
lasts 6.25 ps, five time constants. The junction no longer settles fully within a bit, the traces
split according to the preceding bits, and the dynamic extinction ratio falls to 6.25 dB. With
f_3dB=None both rates give 6.95 dB, so this closing of the eye comes from the junction RC.
With the output phase follows the power, plus a constant. With the envelope convention above, a factor in sits at the physical frequency , so the instantaneous frequency offset of the output is
The frequency moves only on the edges of the signal, where the power changes. With a positive the rising edge shifts to higher frequencies (blue) and the falling edge to lower frequencies (red); a negative reverses both. The sign matters in a dispersive fiber [4]. In standard single-mode fiber at 1550 nm, red light travels slower than blue, so with a negative the red rising edge and the blue falling edge move toward the center of the pulse, and the pulse first compresses. A positive broadens it from the start.
The chirp parameter of an EAM changes with the bias and the wavelength, because both move the operating point along the absorption edge, and it is usually taken from a measurement at the operating point. The next cell sends a single 80 Gb/s one bit through modulators with and and plots the output power and the instantaneous frequency, with the expression above for reference.
bit_rate = 80e9 # Bit rate of the drive (b/s)
# Compute the number of samples per bit, the unit intervalunit_interval = int(round(1 / bit_rate / time_step))
# Build a single one bit between zeros, with smoothed edgesv_drive = np.repeat( np.where(np.array([0, 0, 1, 0, 0]) == 1, v_pp / 2, -v_pp / 2), unit_interval)width = edge_fraction * unit_intervalk = np.arange(-4 * width, 4 * width + 1)kernel = np.exp(-0.5 * (k / width) ** 2)v_drive = np.convolve( np.pad(v_drive, k.size, mode="edge"), kernel / kernel.sum(), "same")v_drive = v_drive[k.size : -k.size]
# Set the time axis in bits, centered on the one bitt_bits = np.arange(v_drive.size) / unit_interval - 2.5
# Select the central three bits, after the junction has charged to the biasshown = np.abs(t_bits) < 1.5
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), tight_layout=True)for alpha_c in [-0.5, 0.5]: # Create the modulator with this chirp parameter chirped = pfa.electro_absorption_modulator( absorption=absorption, length=length, confinement_factor=confinement, bias=bias, residual_loss=transition_loss, f_3dB=f_rc, chirp=alpha_c, z0=z0, )
# Run it on the CW input and the single-bit drive time_stepper = chirped.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) bit_inputs = pf.TimeSeries( { "P0@0": np.full(v_drive.size, np.sqrt(p_in), dtype=complex), "E0@0": v_drive / np.sqrt(z0) + 0j, }, time_step, ) a_out = time_stepper.step(inputs=bit_inputs, show_progress=False)["P1@0"]
# Compute the instantaneous frequency offset from the output phase (GHz) phase = np.unwrap(np.angle(a_out)) delta_nu = -np.gradient(phase, time_step) / (2 * np.pi) / 1e9
# Compute the same offset from the logarithmic derivative of the power (GHz) p_out = np.abs(a_out) ** 2 delta_nu_law = alpha_c / (4 * np.pi) * np.gradient(np.log(p_out), time_step) / 1e9
# Plot the power in the left panel and the frequency offset in the right panel label = f"$\\alpha_c$ = {alpha_c:g}" ax1.plot(t_bits[shown], p_out[shown] * 1e6, label=label) ax2.plot(t_bits[shown], delta_nu[shown], label=label) ax2.plot(t_bits[shown], delta_nu_law[shown], "k:", lw=1)
# Find the largest frequency offset on the rising and on the falling edge (GHz) rising = delta_nu[shown & (t_bits < 0)] falling = delta_nu[shown & (t_bits > 0)] peak_rising = rising[np.argmax(np.abs(rising))] peak_falling = falling[np.argmax(np.abs(falling))] print( f"alpha_c = {alpha_c:g}: rising edge {peak_rising:.1f} GHz, " f"falling edge {peak_falling:.1f} GHz" )
# Label the power and frequency panelsax1.set(xlabel="Time (bits)", ylabel="Output power (µW)")ax1.legend(fontsize=7)ax2.set(xlabel="Time (bits)", ylabel="Frequency offset (GHz)")ax2.legend(fontsize=7)plt.show()alpha_c = -0.5: rising edge -11.6 GHz, falling edge 11.9 GHzalpha_c = 0.5: rising edge 11.6 GHz, falling edge -11.9 GHz
The output power is the same for both values of , since the chirp acts only on the phase. The frequency moves only on the edges, where it follows the logarithmic derivative of the power. With the rising edge shifts by +11.6 GHz, to the blue, and the falling edge by -11.9 GHz, to the red; reverses both shifts.
Summary
Section titled “Summary”pfa.electro_absorption_modulatoris an EAM with two optical ports and one electrical port. It carries anAnalyticWaveguideModelfor frequency-domain simulations at a fixed bias and anElectroAbsorptionModTimeStepperfor time-domain simulations driven through the electrical port.- The absorption is a table over voltage and wavelength, sliced at the carrier wavelength.
pf.franz_keldysh_absorptiongenerates it for a bulk absorber such as germanium; itsabsorption_scaleis calibrated to one measured loss. Quantum-well devices use a measured table instead. - The transmission is times the bias-independent
residual_loss, so the transfer curve in dB follows the absorption against voltage. - The drive voltage is on the electrical port, added to
bias. Reverse bias is negative. f_3dBis the RC cutoff of the junction and sets the modulation bandwidth.- The absorbed light produces a photocurrent, emitted on the electrical port as
, with
quantum_efficiencycalibrated to a measured responsivity. chirpis the modulator chirp parameter as quoted on datasheets, which shifts the frequency on the edges of the signal by : a positive value blue-shifts the rising edge.