Mux/Demux Model

f1534fd3e9474cc89301733a7d677703

Wavelength-division multiplexers and demultiplexers (WDM mux/demux) are essential blocks in datacom transceivers, LiDAR, and optical computing circuits. Instead of simulating a full AWG or cascaded-filter layout, PhotonForge provides MuxDemuxModel: an analytic compact model for a single-mode WDM device with one common port and one port per channel.

Each channel behaves as a super-Gaussian band-pass filter centered at \(f_k\) with 3 dB bandwidth \(B_k\):

\[\lvert S_{k,\mathrm{common}}(f) \rvert^2 = 10^{-IL_k/10} \, \exp\left[ -\ln 2 \left( \frac{2 \, \lvert f - f_k \rvert}{B_k} \right)^{n} \right]\]

where \(IL_k\) is the insertion loss (dB) and \(n\) is the filter order. The model is reciprocal, so the same component works as a multiplexer or a demultiplexer. This guide covers creating the model, shaping the passbands, per-channel parameters, and temperature effects.

Setup

[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf
[2]:
tech = pf.basic_technology()
pf.config.default_technology = tech

wavelengths = np.linspace(1.52, 1.57, 501)
freqs = pf.C_0 / wavelengths

Creating the Model

The model can be specified with either frequencies (with bandwidth in Hz) or wavelengths (with bandwidth in μm), but not both. The number of entries defines the number of channels, and the component it is attached to must have one port per channel plus the common port.

For quick exploration we use black_box_component, which creates a component with the right number of ports without any layout. Here we build a 4-channel demux on a 10 nm grid with 200 GHz (about 1.6 nm) passbands:

[3]:
channel_wavelengths = [1.53, 1.54, 1.55, 1.56]

model = pf.MuxDemuxModel(
    wavelengths=channel_wavelengths,
    bandwidth=0.0016,  # in μm, because the channels are given in wavelengths
    order=4,
    insertion_loss=0.5,
)

mux = model.black_box_component(port_spec="Strip")
mux
[3]:
../_images/guides_MuxDemux_Model_5_0.svg

Port convention

When ports is not set, the model uses the sorted list of the component’s port names, and the first one is the common port. In the black box above, P0 is the common port and P1 to P4 carry the channels in the order they appear in wavelengths. To attach the model to a component whose port names do not sort conveniently, pass the list explicitly, for example ports=["opt_in", "ch1", "ch2", "ch3", "ch4"], always with the common port first.

Computing the S-matrix shows one passband per channel:

[4]:
s = mux.s_matrix(freqs)

fig, ax = plt.subplots(figsize=(7, 3.5))
for i, wl in enumerate(channel_wavelengths):
    t = np.abs(s[(f"P{i + 1}@0", "P0@0")]) ** 2
    ax.plot(wavelengths, 10 * np.log10(np.maximum(t, 1e-12)), label=f"P0 → P{i + 1} ({wl} μm)")
ax.set_xlabel("Wavelength (μm)")
ax.set_ylabel("Transmission (dB)")
ax.set_ylim(-50, 2)
ax.legend(fontsize=8)
ax.grid(alpha=0.3)
plt.show()
../_images/guides_MuxDemux_Model_7_0.png

Passband Shape: Order and Bandwidth

The order parameter controls the passband shape. Order 2 is a Gaussian, and higher orders produce increasingly flat-top responses with steeper skirts, mimicking higher-order cascaded filters. Regardless of order, the transmission is always exactly 3 dB down at \(f_k \pm B_k/2\):

[5]:
fig, ax = plt.subplots(figsize=(7, 3.5))
for order in [1, 2, 4, 8]:
    single = pf.MuxDemuxModel(wavelengths=[1.55], bandwidth=0.0016, order=order)
    bb = single.black_box_component(port_spec="Strip")
    s1 = bb.s_matrix(freqs)
    t = np.abs(s1[("P1@0", "P0@0")]) ** 2
    ax.plot(wavelengths, 10 * np.log10(np.maximum(t, 1e-12)), label=f"order = {order}")
ax.axhline(-3, color="k", linestyle=":", alpha=0.5)
ax.axvline(1.55 - 0.0008, color="k", linestyle=":", alpha=0.5)
ax.axvline(1.55 + 0.0008, color="k", linestyle=":", alpha=0.5)
ax.set_xlim(1.545, 1.555)
ax.set_ylim(-40, 2)
ax.set_xlabel("Wavelength (μm)")
ax.set_ylabel("Transmission (dB)")
ax.legend()
ax.grid(alpha=0.3)
plt.show()
../_images/guides_MuxDemux_Model_9_0.png

Per-Channel Parameters

bandwidth, order, insertion_loss, and group_delay accept a sequence with one value per channel. The reflection and ports sequences need one extra entry for the common port, which comes first. Here each channel gets a different bandwidth and loss:

[6]:
model_pc = pf.MuxDemuxModel(
    wavelengths=channel_wavelengths,
    bandwidth=[0.0008, 0.0012, 0.0016, 0.0024],
    insertion_loss=[0.5, 1.0, 1.5, 2.0],
    order=4,
)

mux_pc = model_pc.black_box_component(port_spec="Strip")
s_pc = mux_pc.s_matrix(freqs)

fig, ax = plt.subplots(figsize=(7, 3.5))
for i, wl in enumerate(channel_wavelengths):
    t = np.abs(s_pc[(f"P{i + 1}@0", "P0@0")]) ** 2
    ax.plot(wavelengths, 10 * np.log10(np.maximum(t, 1e-12)), label=f"channel {i + 1}")
ax.set_xlabel("Wavelength (μm)")
ax.set_ylabel("Transmission (dB)")
ax.set_ylim(-50, 2)
ax.legend(fontsize=8)
ax.grid(alpha=0.3)
plt.show()
../_images/guides_MuxDemux_Model_11_0.png

A constant group_delay (in seconds) adds a linear phase to each channel, which matters for time-domain simulations, and reflection sets the complex reflection coefficient seen by incident fields at each port.

Temperature Sensitivity

Real WDM filters drift with temperature. Setting temperature_sensitivity shifts every channel center by \(s \cdot (T - T_\mathrm{ref})\), where the units are Hz/K when the model uses frequencies and μm/K when it uses wavelengths. A typical silicon filter drifts by roughly 0.08 nm/K:

[7]:
fig, ax = plt.subplots(figsize=(7, 3.5))
for temp, style in [(293.0, "-"), (343.0, "--")]:
    model_t = pf.MuxDemuxModel(
        wavelengths=channel_wavelengths,
        bandwidth=0.0016,
        order=4,
        temperature_sensitivity=8e-5,  # μm/K = 0.08 nm/K
        temperature=temp,
        reference_temperature=293.0,
    )
    bb_t = model_t.black_box_component(port_spec="Strip")
    s_t = bb_t.s_matrix(freqs)
    t1 = np.abs(s_t[("P1@0", "P0@0")]) ** 2
    ax.plot(wavelengths, 10 * np.log10(np.maximum(t1, 1e-12)), style, label=f"channel 1, T = {temp:.0f} K")
ax.set_xlabel("Wavelength (μm)")
ax.set_ylabel("Transmission (dB)")
ax.set_ylim(-50, 2)
ax.legend()
ax.grid(alpha=0.3)
plt.show()
../_images/guides_MuxDemux_Model_13_0.png

The 50 K increase shifts the channel by 4 nm, enough to move a signal into the neighboring channel, which is why athermal designs or active tuning are needed in dense WDM systems.

Abstract Component

For system-level sketches, abstract.mux_demux wraps this model in a ready-made schematic component. Its defaults follow a DWDM-style 100 GHz grid, and all model parameters can be overridden:

[8]:
demux = pf.abstract.mux_demux(
    frequencies=pf.C_0 / np.array(channel_wavelengths),
    bandwidth=200e9,  # in Hz, because the channels are given in frequencies
    insertion_loss=0.5,
)
demux
[8]:
../_images/guides_MuxDemux_Model_15_0.svg

The abstract component can be connected to lasers, modulators, and photodetectors from the same abstract module to assemble a full WDM link for S-matrix or time-domain analysis.

Summary

  • MuxDemuxModel gives each channel a super-Gaussian passband: set centers with frequencies (Hz) or wavelengths (μm), width with bandwidth, and shape with order.

  • The first port (in sorted order, or the first entry of ports) is the common port; the rest map to channels in order.

  • Per-channel sequences are accepted for most parameters; reflection and ports take one extra leading entry for the common port.

  • temperature_sensitivity models thermal drift of the channel centers.

  • Use black_box_component or pf.abstract.mux_demux for quick circuit-level studies before committing to a physical demux layout.