Semiconductor Optical Amplifier
![]()
A semiconductor optical amplifier (SOA) is a semiconductor waveguide whose core is an electrically pumped active region, typically InGaAsP quantum wells on InP. The injected carriers give the guide optical gain, and light is amplified in a single pass. SOAs are used as booster amplifiers after a transmitter, as preamplifiers before a receiver, and as gain sections in III-V and heterogeneously integrated photonic circuits.
The gain of an SOA depends on the optical power passing through it. Strong light consumes carriers faster than the pump current replaces them, so the gain falls: this is gain saturation. The carriers recover with the carrier lifetime , typically 100 ps to 500 ps, which is comparable to the bit period of a data signal. The gain at any instant therefore depends on the recent history of the signal. The carrier density also sets the refractive index, so every change of gain comes with a change of phase, which chirps the amplified light.
SemiconductorAmplifierTimeStepper
models these effects together with amplified spontaneous emission (ASE), and
semiconductor_amplifier wraps
it in a two-port component that amplifies from P0 to P1. This guide shows how to use it for
gain saturation under continuous-wave (CW) light, the amplification of short pulses, gain
recovery, the pattern effect on non-return-to-zero (NRZ) data, and the chirp of amplified pulses.
The constant-gain amplifier and the ASE noise model are covered in the
Optical Amplifier guide.
What the time stepper computes
Section titled “What the time stepper computes”The model follows Agrawal and Olsson [1]. Let be the carrier density in the active region, the pump current, the elementary charge and the active volume. The carriers are supplied by the current, recombine with the lifetime and are consumed by stimulated emission:
where is the optical power, the photon energy, the cross-section of the active region, and the modal gain coefficient. Near transparency the gain is linear in the carrier density, , with the confinement factor, the differential gain and the transparency density. Multiplying the rate equation by turns it into an equation for the gain itself:
Here is the unsaturated gain set by the current and is the saturation energy. The power grows along the device as . Integrating the gain equation over the length and writing for the integrated gain, the term becomes . With this gives a single ordinary differential equation, the one the time stepper integrates:
Time is measured in a frame that moves with the light, so the transit time through the device does not appear. The instantaneous power gain is .
The same carriers change the refractive index. The linewidth enhancement factor , also called Henry’s factor, is the ratio of the index change to the gain change, and it makes the optical field acquire a phase . The output field is
with . ASE is added to the output with the one-sided spectral density , as in the Optical Amplifier guide, but evaluated at the instantaneous gain .
The component’s parameters map onto these quantities as follows:
small_signal_gainis the unsaturated gain , in dB.saturation_poweris the internal saturation power , in W. Setting it toNonedisables saturation.carrier_lifetimeis , in s.linewidth_enhancement_factoris . A value of 0 disables the chirp.noise_figureis the noise figure NF in dB, which sets . A value ofNonedisables ASE.
The gain is spectrally flat around the carrier frequency, and effects faster than about a picosecond, such as carrier heating and spectral hole burning, are not included.
Reference
- Agrawal, G. P., and Olsson, N. A. “Self-phase modulation and spectral broadening of optical pulses in semiconductor laser amplifiers.” IEEE Journal of Quantum Electronics 1989 25 (11), 2297-2306, doi: 10.1109/3.42059.
- Connelly, M. J. Semiconductor Optical Amplifiers. Kluwer Academic Publishers 2002.
We use an InP SOA with 20 dB of small-signal gain, a 10 mW internal saturation power, a 200 ps carrier lifetime, and a 7 dB noise figure. These are typical of a quantum-well SOA at 1550 nm. The saturation energy is then pJ, which sets the scale for every pulse below.
import matplotlib.pyplot as pltimport numpy as npimport photonforge as pfimport photonforge.abstract as pfa
# Open the live viewer to display the amplifier componentviewer = pf.live_viewer.LiveViewer()
# Use the basic technology for the abstract componentspf.config.default_technology = pf.basic_technology()
# Set the optical carrier at 1550 nm (pf.C_0 is the speed of light in um/s)f0 = pf.C_0 / 1.55
# Planck constant, used for the ASE photon energyh_planck = 6.62607015e-34
# Set the amplifier parametersg0_db = 20.0 # Small-signal gain (dB)p_sat = 10e-3 # Internal saturation power (W)tau_c = 200e-12 # Carrier lifetime (s)alpha_h = 5.0 # Linewidth enhancement factornf_db = 7.0 # Noise figure (dB)
# Convert the small-signal gain to a linear power ratiog0 = 10 ** (g0_db / 10)
# Compute the saturation energy E_sat = P_sat tau_c (J)e_sat = p_sat * tau_cprint(f"Saturation energy: {e_sat * 1e12:.1f} pJ")LiveViewer started at http://127.0.0.1:40631. Call its 'stop()' method to free the port.Saturation energy: 2.0 pJCreating the amplifier
Section titled “Creating the amplifier”pfa.semiconductor_amplifier returns a two-port component that carries the time stepper. The
component has no frequency-domain model, since a saturated gain depends on the signal and has no
S matrix; it is used in time-domain simulations only.
# Create the SOA with gain saturation, chirp and ASEsoa = pfa.semiconductor_amplifier( small_signal_gain=g0_db, saturation_power=p_sat, carrier_lifetime=tau_c, linewidth_enhancement_factor=alpha_h, noise_figure=nf_db, seed=0,)viewer(soa)A single component needs no surrounding circuit: setup_time_stepper returns its time stepper
directly, and the input field is passed to step as a
TimeSeries keyed by the input port, "P0@0". The
output field is read from "P1@0". The field amplitudes are in , so
is the power in watts.
With saturation and chirp disabled, the SOA reduces to the constant-gain optical_amplifier. The next cell drives both with the same weak CW input, 1 µW, and prints the output power and the ASE power per sample, which is for a time step .
time_step = 0.1e-12 # 0.1 ps step, a 10 THz simulation bandwidthnum_steps = 2**16 # Long record for a clean noise averagep_in = 1e-6 # CW input power: 1 uW
# Build the CW input field in sqrt(W)a_in = np.full(num_steps, np.sqrt(p_in), dtype=complex)
# Create the two amplifiers with the same gain, noise figure and noise seedamplifiers = { "optical_amplifier": pfa.optical_amplifier(gain=g0_db, noise_figure=nf_db, seed=0), "semiconductor_amplifier": pfa.semiconductor_amplifier( small_signal_gain=g0_db, noise_figure=nf_db, seed=0 ),}
for name, amplifier in amplifiers.items(): # Run the amplifier's time stepper on the CW input time_stepper = amplifier.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": a_in}, time_step), show_progress=False )["P1@0"]
# Separate the amplified carrier from the ASE fluctuations carrier = a_out.mean() ase_power = np.mean(np.abs(a_out - carrier) ** 2) print( f"{name}: output power {np.abs(carrier) ** 2 * 1e6:.1f} uW, " f"ASE power {ase_power * 1e6:.1f} uW" )
# Compute the analytic ASE power per sample, S_ASE / time_stepn_sp = 10 ** (nf_db / 10) / 2s_ase = n_sp * (g0 - 1) * h_planck * f0print(f"S_ASE / time_step: {s_ase / time_step * 1e6:.1f} uW")optical_amplifier: output power 101.7 uW, ASE power 317.6 uWsemiconductor_amplifier: output power 101.7 uW, ASE power 317.6 uWS_ASE / time_step: 317.9 uWBoth components produce the same output, sample for sample: about 100 µW of amplified signal plus about 320 µW of ASE across the 10 THz simulation bandwidth. The rest of the guide turns on the effects that only the SOA has, and turns ASE off so that each effect can be seen on its own.
Gain saturation
Section titled “Gain saturation”Under constant input power the gain settles where . With the steady state is
This is implicit in , but it can be solved for the input power instead, , which gives the analytic saturation curve directly: pick a gain, compute the input power, and multiply by the gain for the output power.
Datasheets quote the saturation output power , the output power at which
the gain has fallen 3 dB below . It is not the same quantity as the internal
of the model. Setting in the steady state gives
, and the function
saturation_output_power performs this
conversion. To match a datasheet, adjust saturation_power until it returns the quoted value.
The next cell sweeps the CW input power from -40 dBm to +10 dBm. Each run lasts 2 ns, ten carrier lifetimes, so the gain has settled by the last sample.
time_step = 1e-12 # 1 ps step, small against the 200 ps carrier lifetimenum_steps = 2000 # 2 ns, ten carrier lifetimes
# Sweep the CW input power (dBm)p_in_dbm = np.linspace(-40, 10, 26)p_in = 1e-3 * 10 ** (p_in_dbm / 10)
# Create the SOA with gain saturation onlysoa = pfa.semiconductor_amplifier( small_signal_gain=g0_db, saturation_power=p_sat, carrier_lifetime=tau_c)
gain = []for power in p_in: # Run each input power from the unsaturated state time_stepper = soa.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_in = np.full(num_steps, np.sqrt(power), dtype=complex) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": a_in}, time_step), show_progress=False )["P1@0"]
# Read the settled gain from the last sample gain.append(np.abs(a_out[-1]) ** 2 / power)gain = np.array(gain)p_out = gain * p_in
# Compute the analytic curve by choosing the gain and solving for the input powerg_analytic = np.linspace(g0 * 0.999, 2, 500)p_in_analytic = p_sat * np.log(g0 / g_analytic) / (g_analytic - 1)p_out_analytic = g_analytic * p_in_analytic
# Convert the internal saturation power to the 3 dB output saturation powerp_out_3db = pf.saturation_output_power(g0_db, p_sat)print(f"3 dB output saturation power: {10 * np.log10(p_out_3db / 1e-3):.2f} dBm")
# Plot the simulated gain against output power, with the steady-state lawfig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)ax.plot(10 * np.log10(p_out / 1e-3), 10 * np.log10(gain), "o", label="Simulation")ax.plot( 10 * np.log10(p_out_analytic / 1e-3), 10 * np.log10(g_analytic), "k:", label="Steady-state law",)
# Mark the 3 dB gain compression pointax.axhline(g0_db - 3, color="gray", lw=0.8)ax.axvline(10 * np.log10(p_out_3db / 1e-3), color="gray", lw=0.8)ax.set(xlabel="Output power (dBm)", ylabel="Gain (dB)", xlim=(-20, 14), ylim=(9, 21))ax.legend()plt.show()3 dB output saturation power: 8.50 dBm
The gain stays at 20 dB while the output power is small against and falls steeply once the output reaches a few milliwatts. The gray lines mark the 3 dB point: an internal saturation power of 10 mW (10 dBm) corresponds to a saturation output power of 8.5 dBm on a datasheet.
Amplifying short pulses
Section titled “Amplifying short pulses”A pulse much shorter than passes before the carriers can recover, so the relaxation term in the gain equation can be dropped during the pulse: . This equation integrates in closed form. With the input energy that has arrived by time ,
The leading edge of the pulse sees the full gain , and every later part sees a gain already depleted by the energy ahead of it. Integrating over the pulse gives the output energy for an input energy [1]:
The energy gain depends only on and not on the pulse shape. The next cell amplifies 10 ps Gaussian pulses with input energies from 0.001 to 10 , and plots the energy gain with this law for reference.
time_step = 0.1e-12 # 0.1 ps step, 100 samples across the pulsefwhm = 10e-12 # Pulse duration: full width at half maximum of the power (s)
# Set the time axis, with room after the pulse for the full outputt = np.arange(-100e-12, 400e-12, time_step)
# Build a Gaussian power shape that integrates to 1, so power = energy * shapeshape = np.exp(-4 * np.log(2) * (t / fwhm) ** 2)shape /= shape.sum() * time_step
# Sweep the input pulse energy relative to the saturation energye_in = e_sat * np.logspace(-3, 1, 17)
# Pick three energies to plot the output pulse shapeshown = {1e-2: "C0", 1e-1: "C1", 1.0: "C2"}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), tight_layout=True)e_out = []for energy in e_in: # Run each pulse from the unsaturated state time_stepper = soa.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_in = np.sqrt(energy * shape).astype(complex) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": a_in}, time_step), show_progress=False )["P1@0"]
# Integrate the output power to get the output energy p_out = np.abs(a_out) ** 2 e_out.append(p_out.sum() * time_step)
# Plot the normalized output shape for the selected energies ratio = energy / e_sat for key, color in shown.items(): if np.isclose(ratio, key): label = f"$E_{{in}}$ = {ratio:g} $E_{{sat}}$" ax1.plot(t * 1e12, p_out / p_out.max(), color=color, label=label)e_out = np.array(e_out)
# Compute the closed-form energy gain for comparisone_out_law = e_sat * np.log(1 + g0 * (np.exp(e_in / e_sat) - 1))
# Add the input pulse shape to the left panelax1.plot(t * 1e12, shape / shape.max(), "k:", label="Input")ax1.set(xlabel="Time (ps)", ylabel="Normalized power", xlim=(-20, 20))ax1.legend(fontsize=7)
# Plot the energy gain against the input energy in the right panelax2.semilogx(e_in / e_sat, 10 * np.log10(e_out / e_in), "o", label="Simulation")ax2.semilogx(e_in / e_sat, 10 * np.log10(e_out_law / e_in), "k:", label="Closed form")ax2.set(xlabel="$E_{in}$ / $E_{sat}$", ylabel="Energy gain (dB)")ax2.legend(fontsize=7)plt.show()
# Print the energy gain at the saturation energyi_sat = np.argmin(np.abs(e_in - e_sat))print( f"Energy gain at E_in = E_sat: {10 * np.log10(e_out[i_sat] / e_in[i_sat]):.2f} dB " f"(closed form {10 * np.log10(e_out_law[i_sat] / e_in[i_sat]):.2f} dB)")
Energy gain at E_in = E_sat: 7.25 dB (closed form 7.12 dB)The weakest pulses gain the full 20 dB, and the energy gain falls to 7.2 dB at . The simulation sits about 0.1 dB above the closed form because a 10 ps pulse is 5% of the carrier lifetime, so the carriers recover slightly during the pulse, which the closed form neglects.
The left panel shows the cost in pulse shape. A strongly saturating pulse depletes the gain with its leading edge, so the trailing edge is amplified less: the output pulse is asymmetric and its peak moves earlier in time.
Gain recovery
Section titled “Gain recovery”After a pulse has passed, the gain returns to its unsaturated value. When the remaining input is weak, the extraction term is negligible and the gain equation reduces to . The integrated gain, and so the gain in dB, recovers exponentially with the carrier lifetime.
A CW beam in the amplifier speeds this up. Linearizing the gain equation around a steady state with gain under a CW power gives an effective recovery time
because the extra stimulated emission pulls the carrier density back towards its steady state faster. Such a holding beam is a common way to make an SOA respond faster, at the cost of a lower steady-state gain.
The next cell sends a 2 pJ pulse, one saturation energy, through the SOA on top of a CW input of 1 µW and of 1 mW. The instantaneous gain is the ratio of output to input power. Each run starts 2 ns before the pulse so that the CW beam has settled the gain first.
time_step = 0.5e-12 # 0.5 ps step, 20 samples across the pulse
# Start 2 ns before the pulse so that the CW beam settles the gaint = np.arange(-2000e-12, 1000e-12, time_step)
# Build the pump pulse: one saturation energy with a 10 ps durationpulse = np.exp(-4 * np.log(2) * (t / fwhm) ** 2)pulse *= e_sat / (pulse.sum() * time_step)
# Set the two CW powers with their plot labelscw_powers = {1e-6: "CW power 1 µW", 1e-3: "CW power 1 mW"}
fig, ax = plt.subplots(figsize=(7, 3.2), tight_layout=True)for p_cw, label in cw_powers.items(): # Add the pulse on top of the CW beam a_in = np.sqrt(p_cw + pulse).astype(complex)
# Run the SOA on the combined input time_stepper = soa.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": a_in}, time_step), show_progress=False )["P1@0"]
# Compute the instantaneous gain in dB gain_db = 10 * np.log10(np.abs(a_out) ** 2 / np.abs(a_in) ** 2) ax.plot(t * 1e12, gain_db, label=label)
# Read the steady-state gain 100 ps before the pulse g_ss_db = gain_db[np.searchsorted(t, -100e-12)]
# Compute the effective recovery time from the linearized gain equation tau_eff = tau_c / (1 + 10 ** (g_ss_db / 10) * p_cw / p_sat)
# Find when the gain is back within 1 dB of its steady state i_min = np.argmin(gain_db) t_1db = t[i_min + np.argmax(gain_db[i_min:] > g_ss_db - 1)] print( f"{label}: steady-state gain {g_ss_db:.1f} dB, " f"tau_eff {tau_eff * 1e12:.0f} ps, within 1 dB after {t_1db * 1e12:.0f} ps" )
# Draw the exponential recovery with tau_c after the pulse, for the weak CW input if p_cw == 1e-6: t_ref = t[t > 50e-12] g_start = gain_db[np.searchsorted(t, 50e-12)] decay = np.exp(-(t_ref - 50e-12) / tau_c) ax.plot( t_ref * 1e12, g0_db + (g_start - g0_db) * decay, "k:", label="$\\tau_c$ law" )
# Label the axes around the pulse and its recoveryax.set(xlabel="Time (ps)", ylabel="Gain (dB)", xlim=(-100, 1000))ax.legend()plt.show()CW power 1 µW: steady-state gain 20.0 dB, tau_eff 198 ps, within 1 dB after 577 psCW power 1 mW: steady-state gain 12.6 dB, tau_eff 71 ps, within 1 dB after 219 ps
With the weak input the gain drops from 20 dB to 2.6 dB during the pulse and recovers along the exponential set by , taking about 580 ps to come back within 1 dB. The 1 mW holding beam lowers the steady-state gain to 12.6 dB and cuts the effective recovery time to 71 ps, so the gain is back within 1 dB after about 220 ps. Far from the steady state, just after the pulse, the recovery is slower than predicts, because the linearization only holds close to the steady state.
Pattern effect on NRZ data
Section titled “Pattern effect on NRZ data”The gain responds to the signal with the time constant , so a data signal with a comparable bit period modulates the gain it passes through. A long run of zeros lets the gain recover, so the next one is amplified more; a long run of ones depletes the gain, so the last ones of the run are amplified less. This is the pattern effect, and it spreads the one level of the eye.
The next cell sends NRZ data with a 10 dB extinction ratio and a -10 dBm average input power, which drives the SOA 2.5 dB into compression, at 2.5 Gb/s and at 10 Gb/s. The bits are random, and the edges are smoothed with a Gaussian filter to give a realistic rise time. The output is normalized to its average power, so the two eyes can be compared directly.
time_step = 1e-12 # 1 ps stepnum_bits = 512 # Number of random bitsnum_skip = 16 # Bits discarded at the start while the gain settlesextinction_ratio_db = 10.0 # Ratio of the one to the zero power level (dB)p_avg = 1e-4 # Average input power: -10 dBmedge_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)
# Map the bits to power levels with the chosen extinction ratiolevels = np.where(bits == 1, 1.0, 10 ** (-extinction_ratio_db / 10))
fig, axes = plt.subplots(1, 2, figsize=(7, 3), sharey=True, tight_layout=True)for ax, bit_rate in zip(axes, [2.5e9, 10e9]): # Compute the number of samples per bit, the unit interval unit_interval = int(round(1 / bit_rate / time_step))
# Hold each level for one bit, then smooth the edges with a Gaussian filter p_in = np.repeat(levels, unit_interval) width = edge_fraction * unit_interval k = np.arange(-4 * width, 4 * width + 1) kernel = np.exp(-0.5 * (k / width) ** 2) p_in = np.convolve(p_in, kernel / kernel.sum(), mode="same")
# Scale the pattern to the chosen average power p_in *= p_avg / p_in.mean()
# Run the SOA on the data signal time_stepper = soa.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": np.sqrt(p_in).astype(complex)}, time_step), show_progress=False, )["P1@0"]
# Normalize the output to its average power, skipping the settling bits p_out = np.abs(a_out[num_skip * unit_interval :]) ** 2 p_out /= p_out.mean()
# Fold the output into two-bit segments, centered on the bits, to form the eye segments = p_out[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, "C0", alpha=0.15, lw=0.8) ax.set(title=f"{bit_rate / 1e9:g} Gb/s", xlabel="Time (bits)")
# Measure the spread of the one level at the bit centers centers = p_out[np.arange(num_bits - num_skip) * unit_interval + unit_interval // 2] ones = centers[bits[num_skip:] == 1] spread = 10 * np.log10(ones.max() / ones.min()) print(f"{bit_rate / 1e9:g} Gb/s: one-level spread {spread:.2f} dB")
axes[0].set_ylabel("Normalized output power")plt.show()2.5 Gb/s: one-level spread 0.44 dB10 Gb/s: one-level spread 1.65 dB
The two bit rates show the two faces of the pattern effect:
- At 2.5 Gb/s a bit lasts 400 ps, two carrier lifetimes. The gain recovers during each run of zeros and is depleted within the first part of each one, so every rising edge carries an overshoot, but the one level at the bit center is nearly the same for every bit.
- At 10 Gb/s a bit lasts 100 ps, shorter than . The gain carries the memory of several bits, so the one level depends on the preceding pattern and droops through each run of ones. The spread of the one level is about four times larger.
The pattern effect grows with compression. The next cell repeats the 10 Gb/s case over a range of average input powers and plots the spread of the one level against the average output power, with the 3 dB output saturation power marked. This is the curve that sets how hard an SOA booster can be driven for a given penalty.
bit_rate = 10e9unit_interval = int(round(1 / bit_rate / time_step))
# Build the normalized 10 Gb/s pattern once, with smoothed edgesp_pattern = np.repeat(levels, unit_interval)width = edge_fraction * unit_intervalk = np.arange(-4 * width, 4 * width + 1)kernel = np.exp(-0.5 * (k / width) ** 2)p_pattern = np.convolve(p_pattern, kernel / kernel.sum(), mode="same")p_pattern /= p_pattern.mean()
# Sweep the average input power (dBm)p_avg_dbm = np.arange(-30, 1, 2)spread_db = []p_out_avg = []for power_dbm in p_avg_dbm: # Scale the pattern to this average power and run the SOA p_in = 1e-3 * 10 ** (power_dbm / 10) * p_pattern time_stepper = soa.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": np.sqrt(p_in).astype(complex)}, time_step), show_progress=False, )["P1@0"]
# Record the average output power and the spread of the one level p_out = np.abs(a_out[num_skip * unit_interval :]) ** 2 p_out_avg.append(p_out.mean()) centers = p_out[np.arange(num_bits - num_skip) * unit_interval + unit_interval // 2] ones = centers[bits[num_skip:] == 1] spread_db.append(10 * np.log10(ones.max() / ones.min()))
# Plot the one-level spread against the average output powerfig, ax = plt.subplots(figsize=(7, 3), tight_layout=True)ax.plot(10 * np.log10(np.array(p_out_avg) / 1e-3), spread_db, "o-")
# Mark the 3 dB output saturation powerax.axvline(10 * np.log10(p_out_3db / 1e-3), color="gray", lw=0.8)ax.set(xlabel="Average output power (dBm)", ylabel="One-level spread (dB)")plt.show()
Below about -5 dBm of output power the one level spreads by less than 0.2 dB. The spread then grows quickly as the output approaches the saturation output power, marked in gray, where it reaches about 1.9 dB. Beyond that point it levels off near 2 dB and falls slightly: the deeper saturation shortens below the 100 ps bit period, so the gain again settles within each bit, as it did at 2.5 Gb/s.
Chirp and spectral broadening
Section titled “Chirp and spectral broadening”With the output phase is , so the phase follows the gain. The class uses the envelope convention in which a factor in sits at the physical frequency . The instantaneous frequency offset is therefore
and a spectrum computed with np.fft has to have its frequency axis reversed to read physical
frequency. While a pulse depletes the gain, and the light shifts to
lower frequencies, a red shift. Since the gain is depleted during the pulse and recovers only after
it, the whole pulse is red-shifted, and the spectrum develops several peaks, the dominant one on the
red side [1]. This is opposite to the blue-shifted trailing edge of Kerr self-phase modulation in a
fiber.
The next cell amplifies 10 ps pulses of 0.1, 0.5 and 1 with and plots the instantaneous frequency across the pulse and the output spectra.
time_step = 0.1e-12 # 0.1 ps step, a 10 THz simulation bandwidtht = np.arange(-200e-12, 600e-12, time_step)
# Build the Gaussian power shape, normalized to unit energyshape = np.exp(-4 * np.log(2) * (t / fwhm) ** 2)shape /= shape.sum() * time_step
# Create the SOA with gain saturation and chirpsoa_chirp = pfa.semiconductor_amplifier( small_signal_gain=g0_db, saturation_power=p_sat, carrier_lifetime=tau_c, linewidth_enhancement_factor=alpha_h,)
# Reverse the FFT frequency axis to get the physical frequency offset (GHz)freq = -np.fft.fftshift(np.fft.fftfreq(t.size, time_step)) / 1e9
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), tight_layout=True)
# Plot the input spectrum for referencespectrum_in = np.abs(np.fft.fftshift(np.fft.fft(np.sqrt(shape)))) ** 2ax2.plot(freq, spectrum_in / spectrum_in.max(), "k:", label="Input")
for ratio in [0.1, 0.5, 1.0]: # Run the SOA on a pulse with this energy time_stepper = soa_chirp.setup_time_stepper( time_step=time_step, carrier_frequency=f0, show_progress=False ) a_in = np.sqrt(ratio * e_sat * shape).astype(complex) a_out = time_stepper.step( inputs=pf.TimeSeries({"P0@0": a_in}, time_step), 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 label = f"{ratio:g} $E_{{sat}}$" within = np.abs(t) < 15e-12 ax1.plot(t[within] * 1e12, delta_nu[within], label=label)
# Compute the normalized output spectrum spectrum = np.abs(np.fft.fftshift(np.fft.fft(a_out))) ** 2 ax2.plot(freq, spectrum / spectrum.max(), label=label) print( f"E_in = {ratio:g} E_sat: spectral peak at {freq[np.argmax(spectrum)]:.0f} GHz" )
# Label the frequency and spectrum panelsax1.set(xlabel="Time (ps)", ylabel="Frequency offset (GHz)")ax1.legend(fontsize=7)ax2.set(xlabel="Frequency offset (GHz)", ylabel="Normalized spectrum", xlim=(-200, 100))ax2.legend(fontsize=7)plt.show()E_in = 0.1 E_sat: spectral peak at -51 GHzE_in = 0.5 E_sat: spectral peak at -90 GHzE_in = 1 E_sat: spectral peak at -105 GHz
The instantaneous frequency falls across the leading half of the pulse, where the gain is being depleted fastest. It returns to zero on the trailing edge, where little gain is left to remove, and turns slightly positive after the pulse, where the gain starts to recover. The shift reaches more than 100 GHz for the stronger pulses. The spectra show the result: the main peak moves 50 GHz to 110 GHz to the red, and smaller peaks appear on its blue side. They arise because two instants within the pulse carry the same instantaneous frequency, and their contributions to the spectrum interfere.
This chirp matters whenever an SOA feeds a dispersive link or a narrow filter. A filter centered on the carrier frequency cuts the red-shifted light, and dispersion converts the chirp into pulse distortion.
Summary
Section titled “Summary”pfa.semiconductor_amplifieris a two-port SOA for time-domain simulations. Its gain follows the Agrawal and Olsson rate equation for the integrated gain , with the small-signal gain, the internal saturation power, the carrier lifetime, the linewidth enhancement factor and the noise figure as parameters.- With
saturation_power=Noneandlinewidth_enhancement_factor=0it is the constant-gain optical amplifier, including the same ASE noise. saturation_poweris the internal saturation power of the model. Usepf.saturation_output_powerto convert it to the 3 dB output saturation power quoted on datasheets.- Pulses much shorter than the carrier lifetime are amplified according to their energy relative to , and saturating pulses come out asymmetric.
- The gain recovers with the carrier lifetime, and faster with a CW holding beam, with the effective time .
- On data, the finite recovery produces edge overshoot when the bit period is long against the recovery time and a pattern-dependent one level when it is short. The pattern effect grows steeply as the output approaches the saturation output power.
- A nonzero red-shifts saturating pulses and broadens their spectrum.