Skip to content

Additive Backend

The additive backend can be used to calculate path lengths, group delays, and other additive quantities. Unlike the default multiplicative backend (which multiplies S-parameters along paths), the additive backend sums values. This is useful for computing optical path lengths, propagation delays, or any linear quantity that accumulates along the signal path.

from functools import partial

import jax.numpy as jnp
from sax.models import straight
import matplotlib.pyplot as plt

import sax

How the Additive Backend Works

In standard S-parameter simulations, we multiply transmission coefficients along signal paths. For example, if light passes through two waveguides with transmissions \(T_1\) and \(T_2\), the total transmission is \(T_1 \times T_2\).

With the additive backend, values are summed instead of multiplied. This is useful for quantities like: - Path length: Total physical length = \(L_1 + L_2\) - Group delay: Total delay = \(\tau_1 + \tau_2\) - Phase (unwrapped): Total phase = \(\phi_1 + \phi_2\) - Loss (in dB): Total loss = \(\text{Loss}_1 + \text{Loss}_2\)

The additive backend uses the same netlist structure, but interprets the "S-parameter" values as additive quantities.

Path Length Models

Let's define models that return path lengths instead of transmission coefficients. The coupler has a physical length that light traverses regardless of which port it exits from:

def coupler(length=50.0) -> sax.SDict:
    """Coupler model returning path lengths (in μm)."""
    sdict = {
        ("in0", "out0"): length,
        ("in0", "out1"): length,
        ("in1", "out0"): length,
        ("in1", "out1"): length,
    }
    return sax.reciprocal(sdict)


def waveguide(length=50.0) -> sax.SDict:
    """Coupler model returning path lengths (in μm)."""
    sdict = {
        ("in0", "out0"): length,
    }
    return sax.reciprocal(sdict)

def waveguide(length=100.0) -> sax.SDict: """Waveguide model returning path lengths (in μm).""" sdict = { ("in0", "out0"): length, } return sax.reciprocal(sdict)

MZI with Additive Backend

Let's create an MZI (Mach-Zehnder Interferometer) with different arm lengths and use the additive backend to calculate the total path length through each arm:

           ┌─────────────────────────┐
     ┌─────┤    top (500 μm)         ├─────┐
     │     └─────────────────────────┘     │
in ──┤ lft                              rgt├── out
     │     ┌─────────────────────────┐     │
     └─────┤    btm (100 μm)         ├─────┘
           └─────────────────────────┘
mzi, _ = sax.circuit(
    netlist={
        "instances": {
            "lft": coupler,
            "top": partial(waveguide, length=500),
            "btm": partial(waveguide, length=100),
            "rgt": coupler,
        },
        "connections": {
            "lft,out0": "btm,in0",
            "btm,out0": "rgt,in0",
            "lft,out1": "top,in0",
            "top,out0": "rgt,in1",
        },
        "ports": {
            "in0": "lft,in0",
            "in1": "lft,in1",
            "out0": "rgt,out0",
            "out1": "rgt,out1",
        },
    },
    backend="additive",
)

result = mzi()
result
{('in0', 'in0'): [Array([0.], dtype=float64)],
 ('in0', 'out0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('in0', 'out1'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('in1', 'in1'): [Array([0.], dtype=float64)],
 ('in1', 'out0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('in1', 'out1'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('out0', 'in0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('out0', 'in1'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('out0', 'out0'): [Array([0.], dtype=float64)],
 ('out1', 'in0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('out1', 'in1'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
 ('out1', 'out1'): [Array([0.], dtype=float64)]}

Interpreting the Results

The additive backend returns the total path length for each input-output port combination. Let's analyze the results:

# Path through bottom arm: lft (50) + btm (100) + rgt (50) = 200 μm
print(f"in0 -> out0 (bottom arm): {result['in0', 'out0'][0][0]:.1f} μm")

# Path through top arm: lft (50) + top (500) + rgt (50) = 600 μm
print(f"in0 -> out1 (top arm): {result['in0', 'out1'][0][0]:.1f} μm")

# Path length difference (important for MZI interference)
delta_L = result["in0", "out1"][0][0] - result["in0", "out0"][0][0]
print(f"\nPath length difference (ΔL): {delta_L:.1f} μm")
in0 -> out0 (bottom arm): 200.0 μm
in0 -> out1 (top arm): 200.0 μm

Path length difference (ΔL): 0.0 μm

Example: Group Delay Calculation

The additive backend is also useful for computing group delays. Let's define models that return propagation time instead of length:

# Speed of light in vacuum
c = 3e8  # m/s

# Effective group index for silicon waveguide at 1550nm
ng = 4.2


def coupler_delay(length=50.0) -> sax.SDict:
    """Coupler model returning group delay (in ps)."""
    # Convert length from μm to m, compute delay
    delay_ps = (length * 1e-6) / (c / ng) * 1e12
    sdict = {
        ("in0", "out0"): delay_ps,
        ("in0", "out1"): delay_ps,
        ("in1", "out0"): delay_ps,
        ("in1", "out1"): delay_ps,
    }
    return sax.reciprocal(sdict)


def waveguide_delay(length=100.0) -> sax.SDict:
    """Waveguide model returning group delay (in ps)."""
    delay_ps = (length * 1e-6) / (c / ng) * 1e12
    sdict = {
        ("in0", "out0"): delay_ps,
    }
    return sax.reciprocal(sdict)
mzi_delay, _ = sax.circuit(
    netlist={
        "instances": {
            "lft": coupler_delay,
            "top": partial(waveguide_delay, length=500),
            "btm": partial(waveguide_delay, length=100),
            "rgt": coupler_delay,
        },
        "connections": {
            "lft,out0": "btm,in0",
            "btm,out0": "rgt,in0",
            "lft,out1": "top,in0",
            "top,out0": "rgt,in1",
        },
        "ports": {
            "in0": "lft,in0",
            "in1": "lft,in1",
            "out0": "rgt,out0",
            "out1": "rgt,out1",
        },
    },
    backend="additive",
)

delay_result = mzi_delay()
print(f"Group delay through bottom arm: {delay_result['in0', 'out0'][0][0]:.3f} ps")
print(f"Group delay through top arm: {delay_result['in0', 'out1'][0][0]:.3f} ps")
print(
    f"Differential group delay: {delay_result['in0', 'out1'][0][0] - delay_result['in0', 'out0'][0][0]:.3f} ps"
)
Group delay through bottom arm: 2.800 ps
Group delay through top arm: 2.800 ps
Differential group delay: 0.000 ps

Visualization: Comparing Path Lengths

Let's visualize the path lengths for different MZI arm configurations:

top_lengths = jnp.linspace(100, 1000, 10)
bottom_lengths = jnp.array([100.0])  # Fixed bottom arm

path_diff = []
for top_len in top_lengths:
    mzi_test, _ = sax.circuit(
        netlist={
            "instances": {
                "lft": coupler,
                "top": partial(waveguide, length=float(top_len)),
                "btm": partial(waveguide, length=100),
                "rgt": coupler,
            },
            "connections": {
                "lft,out0": "btm,in0",
                "btm,out0": "rgt,in0",
                "lft,out1": "top,in0",
                "top,out0": "rgt,in1",
            },
            "ports": {
                "in0": "lft,in0",
                "in1": "lft,in1",
                "out0": "rgt,out0",
                "out1": "rgt,out1",
            },
        },
        backend="additive",
    )
    result = mzi_test()
    path_diff.append(result["in0", "out1"][0][0] - result["in0", "out0"][0][0])

plt.figure(figsize=(8, 4))
plt.plot(top_lengths, path_diff, "o-", linewidth=2, markersize=8)
plt.xlabel("Top Arm Length (μm)")
plt.ylabel("Path Length Difference ΔL (μm)")
plt.title("MZI Path Length Difference vs Top Arm Length")
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color="k", linestyle="--", alpha=0.5)
plt.show()

png

When to Use the Additive Backend

The additive backend is useful when you need to:

  1. Calculate optical path lengths - Important for interferometer design, determining FSR (Free Spectral Range)
  2. Compute group delays - Essential for high-speed communication systems and pulse propagation
  3. Sum losses in dB - Convenient for loss budgeting when models return loss in dB
  4. Analyze phase accumulation - When you want to track unwrapped phase

The additive backend uses the same circuit analysis infrastructure as the standard backend, so it handles complex circuits with multiple paths correctly. When multiple paths exist between ports, each path's contribution is computed separately.