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,
}
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,out0",
"lft,out1": "top,in0",
"top,out0": "rgt,out1",
},
"ports": {
"in0": "lft,in0",
"out0": "rgt,in0",
},
},
backend="additive",
)
result = mzi()
result
{('in0', 'in0'): [Array([0.], dtype=float64)],
('in0', 'out0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
('out0', 'in0'): [Array([200.], dtype=float64), Array([600.], dtype=float64)],
('out0', 'out0'): [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_lengths = result["in0", "out0"]
print(f"first path: {path_lengths[0][0]:.1f} μm")
print(f"second path: {path_lengths[1][0]:.1f} μm")
delta_L = path_lengths[1][0] - path_lengths[0][0]
print(f"\nPath length difference (ΔL): {delta_L:.1f} μm")
first path: 200.0 μm
second path: 600.0 μm
Path length difference (ΔL): 400.0 μm
When to Use the Additive Backend¶
The additive backend is useful when you need to:
- Calculate optical path lengths - Important for interferometer design, determining FSR (Free Spectral Range)
- Compute group delays - Essential for high-speed communication systems and pulse propagation
- Sum losses in dB - Convenient for loss budgeting when models return loss in dB
- 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.