Optical Peaking in a Microring Modulator

5f667453e9804071992c7db8e6428d2c

The small-signal electro-optic (EO) response of a microring modulator is not a simple low-pass set by the photon lifetime: when the laser is detuned from the cavity resonance, a resonant peak appears in the EO response that extends the modulation bandwidth beyond the photon-lifetime limit (Müller et al.).

Why it happens. Driving the ring’s phase at \(f_m\) creates modulation sidebands at \(f_\text{laser} \pm f_m\), each weighted by the cavity’s Lorentzian response. With the laser parked off-resonance by \(\Delta\), the sideband generated at \(f_m \approx |\Delta|\) lands back on the resonance and is resonantly enhanced instead of filtered; its beat with the carrier boosts the detected modulation at precisely that frequency. At zero detuning both sidebands roll off the Lorentzian flanks symmetrically, and the stored intracavity energy - which can only change as fast as the photon lifetime \(\tau_a\) allows - enforces the familiar single-pole low-pass. The peaking is therefore a sideband-filtering effect inside the optical cavity, fundamentally different from electrical peaking, which arises from reactive (inductive-capacitive) resonance in the drive circuit.

This notebook reproduces the effect with two equivalent time-domain constructions of the same ring and validates both against the closed-form coupled-mode-theory (CMT) expression:

In both cases the EO response is extracted by impulse response: one simulation per detuning - a small electrical pulse is applied and H(f) = FFT(ΔP)/FFT(drive) recovers the entire continuous transfer function.

Reference

    1. Müller, et al. “Optical Peaking Enhancement in High-Speed Ring Modulators” Sci. Rep., 2014 4, 6310, doi: 10.1038/srep06310.

Setup and device parameters

A silicon-scale ring (radius 10 µm) near 1550 nm, coupled near critical coupling for a deep resonance. The loaded \(Q\) (a few thousand) puts the photon-lifetime bandwidth in the tens-of-GHz range, where optical peaking is useful for high-speed links.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf

# virtual port specs reused by every black-box component below
pf.config.default_technology = pf.basic_technology()
opt_port = pf.virtual_port_spec(classification="optical")
elec_port = pf.virtual_port_spec(classification="electrical", impedance=50.0)

# --- ring / waveguide parameters ---
ring_radius = 10.0  # um
ring_length = 2 * np.pi * ring_radius
n_eff0 = 2.4  # effective index at the reference frequency
n_group = 4.2  # group index
loss_db_um = 8e-3  # propagation loss (dB/um) -> sets loaded Q
v_pi_l = 300.0  # V.um  (modulation efficiency of the active section)
lambda0 = 1.55  # um (reference wavelength)
freq0 = pf.C_0 / lambda0
z0 = 50.0  # electrical reference impedance

# near-critical coupling for a deep resonance:
# coupling loss matched to the round-trip propagation loss
round_trip_amp = 10 ** (-loss_db_um * ring_length / 20)
kappa = np.sqrt(1 - round_trip_amp**2)
t_thru = np.sqrt(1 - kappa**2)

# RingModel/RingTimeStepper express modulation as an index shift per volt;
# equivalent to the v_piL of the netlist waveguide:
dn_dv = lambda0 / (2 * v_pi_l)
print(f"kappa = {kappa:.3f}, t = {t_thru:.3f}, dn_dv = {dn_dv:.4e} /V")
kappa = 0.331, t = 0.944, dn_dv = 2.5833e-03 /V

Approach A: ring built as a circuit netlist

The coupler and the active waveguide are separate black-box components closed into a feedback loop; the waveguide carries the PhaseModTimeStepper for time-domain runs. The modulator’s electrical low-pass is disabled (f_3dB=0, the default) so the optical cavity peaking is isolated from any electrical roll-off. This construction generalizes easily (multiple intra-cavity sections, separate active/passive segments).

[2]:
@pf.parametric_component
def create_ring_netlist(n_eff=n_eff0, reference_frequency=freq0, with_time_stepper=False):
    """All-pass microring as coupler + active waveguide in a feedback loop."""
    # point coupler (t, kappa) as a frequency-independent black box
    coupler = pf.DirectionalCouplerModel(t=t_thru, c=-1j * kappa).black_box_component(
        opt_port, name="Coupler"
    )
    # active waveguide closing the loop (frequency-domain model)
    wg_model = pf.AnalyticWaveguideModel(
        n_eff=n_eff,
        n_group=n_group,
        length=ring_length,
        propagation_loss=loss_db_um,
        v_piL=v_pi_l,
        reference_frequency=reference_frequency,
    )
    if with_time_stepper:
        # time-domain twin of the same waveguide, used by the CircuitTimeStepper
        wg_model.time_stepper = pf.PhaseModTimeStepper(
            n_eff=n_eff,
            n_group=n_group,
            length=ring_length,
            v_piL=v_pi_l,
            z0=z0,
            propagation_loss=loss_db_um,
        )  # f_3dB=0 by default -> electrical filter disabled
    wg = wg_model.black_box_component(opt_port, name="RingWG")
    ports = [("dc", "P0", "In"), ("dc", "P2", "Through")]
    if with_time_stepper:
        wg.add_port(pf.Port((0.0, 1.0), -90, spec=elec_port))  # electrical drive port
        ports.append(("wg", "E0", "E_drive"))
    # close the feedback loop: coupler P1 -> waveguide -> coupler P3
    return pf.component_from_netlist(
        {
            "name": "AllPassMRM",
            "instances": {"dc": {"component": coupler}, "wg": {"component": wg}},
            "virtual connections": [
                (("dc", "P1"), ("wg", "P0")),
                (("wg", "P1"), ("dc", "P3")),
            ],
            "ports": ports,
            "models": [(pf.CircuitModel(), "Circuit")],
        }
    )

Approach B: built-in RingModel + RingTimeStepper

The same all-pass ring as one analytic component: RingModel provides the frequency-domain S-matrix and RingTimeStepper simulates the time domain with a bus coupler and a delay line for the round trip - the same dynamics the netlist solves through circuit feedback. Two mapping details:

  • Coupler convention: both use the cross-coupling magnitude \(|\kappa|\), but the built-in through coefficient is \(\tau = -i\,e^{i\arg\kappa}\sqrt{1-|\kappa|^2}\) - the extra \(-90°\) phase per pass shifts the resonance comb by a quarter FSR relative to Approach A. This is physically irrelevant: each construction is simply characterized around its own resonance.

  • Modulation efficiency enters as dn_dv \(= \lambda_0 / (2 V_\pi L)\) instead of v_piL.

[3]:
@pf.parametric_component
def create_ring_builtin(n_eff=n_eff0, reference_frequency=freq0, with_time_stepper=False):
    """All-pass microring as a single built-in RingModel/RingTimeStepper component."""
    model = pf.RingModel(
        kappa1=kappa,  # single bus (kappa2=None)
        n_eff=n_eff,
        length=ring_length,
        propagation_loss=loss_db_um,
        n_group=n_group,
        reference_frequency=reference_frequency,
        dn_dv=dn_dv,  # modulation as linear index shift per volt
        ports=["P0", "P1"],  # P0 = input, P1 = through
    )
    if with_time_stepper:
        # time-domain twin: bus coupler + round-trip delay line
        model.time_stepper = pf.RingTimeStepper(
            kappa1=kappa,
            n_eff=n_eff,
            length=ring_length,
            propagation_loss=loss_db_um,
            n_group=n_group,
            dn_dv=dn_dv,
            z0=z0,
            ports=["P0", "P1"],
            verbose=False,
        )  # f_3dB=0 by default -> electrical filter disabled
    ring = model.black_box_component(opt_port, name="BuiltinRing")
    if with_time_stepper:
        ring.add_port(pf.Port((0.0, 1.0), -90, spec=elec_port))  # electrical drive (E0)
    return ring

Resonance and loaded Q of both rings

Each construction is characterized around its own resonance (quarter-FSR offset between the two, see above). The linewidth, loaded \(Q\), and photon lifetime - the quantities that set the EO response - must agree.

[4]:
def find_resonance(ring, in_name, thru_name):
    """Locate the resonance nearest 1550 nm and measure linewidth/Q from the S-matrix."""
    wl = np.linspace(1.545, 1.560, 8000)
    trans = np.abs(ring.s_matrix(pf.C_0 / wl)[(f"{in_name}@0", f"{thru_name}@0")]) ** 2
    # pick the transmission minimum closest to 1550 nm
    minima = np.where((trans[1:-1] < trans[:-2]) & (trans[1:-1] < trans[2:]))[0] + 1
    ir = minima[np.argmin(np.abs(wl[minima] - lambda0))]
    lam_res = wl[ir]
    # half-depth crossings -> FWHM -> loaded Q and photon lifetime
    half = 0.5 * (trans[ir] + np.median(trans))
    left = ir - np.argmax(trans[ir::-1] >= half)
    right = ir + np.argmax(trans[ir:] >= half)
    fwhm_hz = pf.C_0 / lam_res**2 * (wl[right] - wl[left])
    return {
        "wl": wl,
        "trans": trans,
        "lam_res": lam_res,
        "f_res": pf.C_0 / lam_res,
        "fwhm_hz": fwhm_hz,
        "q": lam_res / (wl[right] - wl[left]),
        "tau_a": 1.0 / (np.pi * fwhm_hz),  # field lifetime (FWHM_int = 1/(pi*tau_a))
    }


res_a = find_resonance(create_ring_netlist(), "In", "Through")
res_b = find_resonance(create_ring_builtin(), "P0", "P1")
photon_bw = 1.0 / (2 * np.pi * res_a["tau_a"])  # EO bandwidth at zero detuning

for name, r in [("A (netlist)", res_a), ("B (built-in)", res_b)]:
    print(
        f"{name}: resonance {r['lam_res']*1e3:.3f} nm   Q {r['q']:.0f}   "
        f"FWHM {r['fwhm_hz']/1e9:.1f} GHz"
    )
print(f"photon-lifetime bandwidth: {photon_bw/1e9:.1f} GHz")

# overlay both line shapes on a common detuning axis
plt.figure(figsize=(8, 4))
for (name, r), style in [(("A (netlist)", res_a), "-"), (("B (built-in)", res_b), "--")]:
    plt.plot(
        (r["wl"] - r["lam_res"]) * 1e3, 10 * np.log10(r["trans"]), style, label=name
    )
plt.xlabel("Detuning from own resonance (nm)")
plt.ylabel("Through transmission (dB)")
plt.title("Same line shape from both constructions")
plt.xlim(-0.3, 0.3)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Progress: 100%
Progress: 100%
A (netlist): resonance 1552.627 nm   Q 4600   FWHM 42.0 GHz
B (built-in): resonance 1550.346 nm   Q 4619   FWHM 41.9 GHz
photon-lifetime bandwidth: 21.0 GHz
../_images/examples_MRM_Optical_Peaking_8_1.png

Coupled-mode reference

Müller et al. derive the small-signal EO response from perturbation of the intracavity field. In the lossless-detuning limit (their Eq. 3) it is a sum of two sideband terms,

\[S_{21,\text{EO}}(\omega_m)\;\propto\;\frac{1}{\tfrac{1}{\tau_a}+i(\omega_m-\Delta)}\;+\;\frac{1}{\tfrac{1}{\tau_a}+i(\omega_m+\Delta)},\]

with cavity field lifetime \(\tau_a\) and detuning \(\Delta=\omega_r-\omega_0\). When \(\omega_m\approx|\Delta|\) the first term collapses to \(1/\tau_a\) and the response peaks; at zero detuning it reduces to a single-pole low-pass at \(1/(2\pi\tau_a)\).

[5]:
def cmt_s21(f_m, detuning_hz, tau_a):
    """Closed-form small-signal EO response (Mueller et al., Eq. 3, symmetric form)."""
    wm = 2 * np.pi * np.asarray(f_m)
    g = 1.0 / tau_a  # cavity field decay rate
    det = 2 * np.pi * detuning_hz
    # two sideband terms; the first one peaks when wm ~ |det|
    return 1.0 / (g + 1j * (wm - det)) + 1.0 / (g + 1j * (wm + det))

Optical peaking vs. detuning - both approaches against CMT

For each detuning, one impulse-response simulation per construction: Approach A (solid), Approach B (dotted), and the CMT model (dashed, using \(\tau_a\) from Approach A - the two values agree to <0.5%). All curves are normalized at 4 GHz.

[8]:
detunings_ghz = [8.0, 16.0, 24.0]  # laser-resonance detunings to compare
f_fine = np.linspace(1e9, 80e9, 400)  # frequency grid for the CMT curves
colors = ["C0", "C1", "C2"]


def to_db(x, ref):
    return 20 * np.log10(np.abs(x) / np.abs(ref))


fig, ax = plt.subplots(figsize=(8, 5))
for det_ghz, col in zip(detunings_ghz, colors):
    det = det_ghz * 1e9
    curves = {}
    for use_builtin in (False, True):  # one simulation per construction
        f_imp, h_imp = impulse_response(det, use_builtin=use_builtin)
        keep = f_imp <= 80e9
        ref_imp = h_imp[np.argmin(np.abs(f_imp - 4e9))]  # normalize at low frequency
        curves[use_builtin] = (f_imp[keep], to_db(h_imp[keep], ref_imp))
    ax.plot(curves[False][0] / 1e9, curves[False][1], "-", color=col,
            label=f"Δ = {det_ghz:.0f} GHz")
    ax.plot(curves[True][0] / 1e9, curves[True][1], ":", color=col, lw=2.2)
    # analytic CMT reference, normalized the same way (dashed)
    h_cmt = cmt_s21(f_fine, det, res_a["tau_a"])
    ax.plot(f_fine / 1e9, to_db(h_cmt, cmt_s21(1e6, det, res_a["tau_a"])),
            "--", color=col, alpha=0.6)
    # quantify approach A vs B agreement on a common grid
    diff = np.abs(
        curves[False][1]
        - np.interp(curves[False][0], curves[True][0], curves[True][1])
    )
    print(f"Δ = {det_ghz:>4.0f} GHz: max |A - B| = {diff.max():.3f} dB")

ax.axhline(-3, color="gray", ls=":")
ax.plot([], [], "k-", label="A: netlist ring")
ax.plot([], [], "k:", lw=2.2, label="B: built-in RingTimeStepper")
ax.plot([], [], "k--", alpha=0.6, label="coupled-mode theory")
ax.set_xlabel("RF modulation frequency (GHz)")
ax.set_ylabel("Normalized EO response (dB)")
ax.set_title("Optical peaking: two time-domain constructions vs. coupled-mode theory")
ax.legend(ncol=2, fontsize=8)
ax.grid(True, alpha=0.3)
ax.set_ylim(-12, 6)
ax.set_xlim(0, 80)
fig.tight_layout()
plt.show()

print(f"photon-lifetime bandwidth (Δ=0): {photon_bw/1e9:.1f} GHz")
Progress: 100%
Progress: 40000/40000
Progress: 100%
Progress: 40000/40000
Δ =    8 GHz: max |A - B| = 0.064 dB
Progress: 100%
Progress: 40000/40000
Progress: 100%
Progress: 40000/40000
Δ =   16 GHz: max |A - B| = 0.105 dB
Progress: 100%
Progress: 40000/40000
Progress: 100%
Progress: 40000/40000
Δ =   24 GHz: max |A - B| = 0.116 dB
../_images/examples_MRM_Optical_Peaking_15_1.png
photon-lifetime bandwidth (Δ=0): 21.0 GHz

Summary

  • Both constructions reproduce optical peaking and match the CMT model through 70 GHz: a clean low-pass at small detuning and a peak near \(f_m\approx|\Delta|\) that extends the bandwidth past the photon-lifetime limit \(1/(2\pi\tau_a)\). The two PhotonForge results also agree with each other to a few hundredths of a dB - they are numerically equivalent descriptions of the same cavity.

  • Approach A (netlist) solves the cavity as explicit circuit feedback (coupler + active waveguide + CircuitTimeStepper): more verbose, but it generalizes to arbitrary intra-cavity topologies (separate active/passive sections, multiple couplers, embedded filters).

  • Approach B (built-in) packs the same physics into a single RingModel/RingTimeStepper component (bus coupler + round-trip delay line, modulation via dn_dv \(=\lambda_0/(2V_\pi L)\)): far more compact when the standard single- or double-bus ring is all you need.

  • The built-in coupler convention (\(\tau = -i\,e^{i\arg\kappa}\sqrt{1-|\kappa|^2}\)) shifts the resonance comb by a quarter FSR relative to the netlist coupler - harmless, since each ring is characterized around its own resonance and driven at the same detunings.

  • The peaking is produced entirely by the cavity dynamics solved in the time domain: no electrical reactance exists anywhere in either circuit (f_3dB=0).