Path-Length Matching
Path-length matching equalises the optical (or electrical) path lengths across a bundle of routes. When routes travel different distances — because start ports are staggered, or end ports are not symmetric — the shorter routes get small "squiggle" loops inserted so that every waveguide in the bundle reaches the same total length.
Why it matters:
- Optical coherence — interferometers and phased arrays require equal path lengths to within a fraction of a wavelength.
- Signal timing — electrical buses must arrive at the same time; unequal lengths cause skew.
- Array uniformity — grating coupler arrays and detector arrays often require matched lengths for balanced response.
How it works in kfactory
Pass kf.PathLengthMatch(...) as a routing constraint to route_bundle, and
tag the bundle with route_name=. After the backbones are computed but before any
instances are placed, the constraint runs path_length_match over the routers
whose route_name it matches. It measures the longest router in the matched set,
then inserts a "squiggle" loop (a 4-point detour made from two 90° bends) into
each shorter router until all lengths are equal.
Setup
from functools import partial
import kfactory as kf
class LAYER(kf.LayerInfos):
WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)
WGCLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2, 0)
L = LAYER()
kf.kcl.infos = L
wg_enc = kf.kcl.get_enclosure(
kf.LayerEnclosure(name="WGSTD_PLM", sections=[(L.WGCLAD, 0, 2_000)])
)
# Euler bend — width and radius in µm
bend90 = kf.factories.euler.bend_euler_factory(kcl=kf.kcl)(
width=0.5,
radius=10,
layer=L.WG,
enclosure=wg_enc,
angle=90,
)
# Straight factory — width and length in DBU
straight_factory = partial(
kf.factories.straight.straight_dbu_factory(kcl=kf.kcl),
layer=L.WG,
enclosure=wg_enc,
)
WG_WIDTH = kf.kcl.to_dbu(0.5) # 500 DBU
# Effective routing radius of the euler bend (larger than the nominal 10 µm)
bend_radius = kf.routing.optical.get_radius(bend90)
1 · Baseline — unequal path lengths
Three North-facing start ports are staggered vertically (each 200 µm shorter than the previous) so that each route naturally has a different path length. Without path-length matching the routes reach their end ports at different total lengths.
c_baseline = kf.KCell("plm_baseline")
# Staggered starts: port 0 is longest, port 2 is shortest
baseline_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
baseline_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
routes_baseline = kf.routing.optical.route_bundle(
c_baseline,
baseline_starts,
baseline_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
)
print("Unmatched lengths (µm):", [round(r.length / 1000, 1) for r in routes_baseline])
c_baseline
Unmatched lengths (µm): [558.0, 738.0, 868.0]
The three routes have clearly different lengths. Path-length matching will bring them all to the longest value.
2 · Basic path-length matching
Pass constraints=[kf.PathLengthMatch(route_names=["..."], element=-1, loop_side=-1,
loops=1, loop_position=0)] plus route_name="..." to insert loops in the last
backbone segment (element=-1), on the left side, centered in the segment.
c_plm = kf.KCell("plm_basic")
plm_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
plm_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
routes_plm = kf.routing.optical.route_bundle(
c_plm,
plm_starts,
plm_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_basic"],
element=-1, # last backbone segment
loop_side=-1, # loops protrude to the left (-1=left, 0=center, 1=right)
loops=1, # one squiggle loop per route
loop_position=0, # loop is centered in the segment
)
],
route_name="plm_basic",
)
print("Matched lengths (µm):", [round(r.length / 1000, 1) for r in routes_plm])
c_plm
Matched lengths (µm): [918.9, 918.9, 918.9]
All three routes now have equal total length. The shorter routes received progressively taller squiggle loops.
3 · loop_side — which side the loop protrudes
| Value | Enum | Effect |
|---|---|---|
-1 |
LoopSide.left |
Loop extends to the left of the route direction |
0 |
LoopSide.center |
Loop is split symmetrically above and below |
1 |
LoopSide.right |
Loop extends to the right of the route direction |
For routes running northward, "left" means west and "right" means east. Choose the side that avoids neighbouring structures.
loop_side = right
Right-side loops protrude east, into the space of the next route in the bundle,
so the routes need to be closer in path length than for left/center loops. Here
we halve the y-stagger (100 µm instead of 200) so the squiggle stays compact
enough to clear the neighbour — verifiable with c.connectivity_check().
c_right = kf.KCell("plm_loop_right")
rs_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 100)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
rs_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
kf.routing.optical.route_bundle(
c_right,
rs_starts,
rs_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_right"],
element=-1,
loop_side=1, # ← right
loops=1,
loop_position=0,
)
],
route_name="plm_right",
)
c_right
loop_side = center (symmetric)
c_center = kf.KCell("plm_loop_center")
lc_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
lc_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
kf.routing.optical.route_bundle(
c_center,
lc_starts,
lc_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_center"],
element=-1,
loop_side=0, # ← center (symmetric around route axis)
loops=1,
loop_position=0,
)
],
route_name="plm_center",
)
c_center
4 · loop_position — where in the segment the loop sits
| Value | Enum | Effect |
|---|---|---|
-1 |
LoopPosition.start |
Loop is placed near the start of the backbone segment |
0 |
LoopPosition.center |
Loop is centered in the backbone segment |
1 |
LoopPosition.end |
Loop is placed near the end of the backbone segment |
c_pos_end = kf.KCell("plm_loop_pos_end")
pe_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
pe_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
kf.routing.optical.route_bundle(
c_pos_end,
pe_starts,
pe_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_pos_end"],
element=-1,
loop_side=-1,
loops=1,
loop_position=1, # ← near the end of the segment
)
],
route_name="plm_pos_end",
)
c_pos_end
5 · loops — multiple squiggle repetitions
When the length difference between the shortest and longest route is large, a single
loop may become very tall. Increasing loops splits the extra length across multiple
smaller squiggles, which keeps the loop footprint compact and avoids collision with
neighbouring structures.
c_loops2 = kf.KCell("plm_loops_2")
l2_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
l2_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
routes_l2 = kf.routing.optical.route_bundle(
c_loops2,
l2_starts,
l2_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_loops_2"],
element=-1,
loop_side=-1,
loops=2, # ← two squiggle loops per route
loop_position=0,
)
],
route_name="plm_loops_2",
)
print("Matched lengths (µm):", [round(r.length / 1000, 1) for r in routes_l2])
c_loops2
Matched lengths (µm): [969.7, 969.7, 969.7]
6 · Inspecting route lengths
route_bundle returns a list of ManhattanRoute objects. Each route exposes
several length properties that are useful for diagnostics:
| Attribute | Description |
|---|---|
route.length |
Total path length in µm (straights + bend arclengths) |
route.length_backbone |
Straight-line length of the backbone |
route.length_straights |
Summed length of all straight segments only |
route.n_bend90 |
Number of 90° bends placed |
route.n_taper |
Number of taper transitions placed |
c_inspect = kf.KCell("plm_inspect")
ins_starts = [
kf.Port(
name=f"in_{i}",
trans=kf.kdb.Trans(1, False, kf.kcl.to_dbu(i * 150), kf.kcl.to_dbu(-i * 200)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i in range(3)
]
ins_ends = [
kf.Port(
name=f"out_{i}",
trans=kf.kdb.Trans(3, False, kf.kcl.to_dbu(x), kf.kcl.to_dbu(400)),
width=WG_WIDTH,
layer_info=L.WG,
)
for i, x in enumerate([170, 300, 380])
]
routes_ins = kf.routing.optical.route_bundle(
c_inspect,
ins_starts,
ins_ends,
separation=kf.kcl.to_dbu(10),
straight_factory=straight_factory,
bend90_cell=bend90,
on_collision=None,
constraints=[
kf.PathLengthMatch(
route_names=["plm_inspect"],
element=-1,
loop_side=-1,
loops=1,
loop_position=0,
)
],
route_name="plm_inspect",
)
print(f"{'Route':<8} {'length (µm)':>14} {'straights (µm)':>16} {'n_bend90':>10}")
print("-" * 52)
for i, r in enumerate(routes_ins):
print(
f" {i:<6} {r.length / 1000:>13.3f}"
f" {r.length_straights / 1000:>15.3f}"
f" {r.n_bend90:>10}"
)
Route length (µm) straights (µm) n_bend90
----------------------------------------------------
0 918.882 730.392 6
1 918.882 730.392 6
2 918.882 730.392 6
All routes share the same total length. The shorter routes have longer length_straights
(the loop adds extra straight segments) and more n_bend90 (the loop uses four bends).
7 · Choosing the right backbone segment (element)
The element key selects which segment of the backbone gets the loop inserted:
element=-1— last segment (before the end port). Best when the end region has ample space and the longest straight is near the end.element=0— first segment (after the start port). Useful when the start region is spacious.- Other integers index into the backbone point list.
The segment must be long enough to contain the loop. A segment shorter than
4 × bend_radius cannot hold a single loop; the router will place the loop but the
geometry may become cramped.
Summary
kf.PathLengthMatch fields:
| Field | Type | Meaning |
|---|---|---|
route_names |
list[str] |
route_names of the bundles to equalise |
element |
int |
Backbone segment index. -1 = last, 0 = first. |
loop_side |
int |
-1 left, 0 center (symmetric), 1 right |
loops |
int |
Number of squiggle repetitions per route |
loop_position |
int |
-1 near start, 0 center, 1 near end of segment |
length |
int \| None |
Target path length (DBU). None = match longest. |
tolerance |
int |
Maximum allowed length spread (DBU) after enforcement |
Practical tips:
- Horizontal separation between routes must be ≥ 2×
bend_radiusso loops do not overlap adjacent waveguides. - For large length differences use
loops=2orloops=3to keep loop heights compact. - Use
route.lengthafter routing to verify that all routes are matched. - Pass
on_collision=Nonein scripts and doc builds to suppress the KLayout error dialog when collision geometry is not critical.
See Also
| Topic | Where |
|---|---|
Bundle routing that kf.PathLengthMatch plugs into |
Routing: Bundle |
| Optical route options including waypoints and stubs | Routing: Optical |
| Routing overview and sub-module map | Routing: Overview |
| Euler bend effective radius (affects loop spacing) | Components: Euler Bends |
| DBU vs µm (route.length is in µm) | Core Concepts: DBU vs µm |