Lumicron Documentation
DocumentationPDK authoringLayer transitions

Layer transitions

When a route’s port_a and port_b live on different layers, the router checks a TransitionRegistry. If the layer pair has a registered factory, the router automatically inserts the resulting PCell - typically an inverse-taper bridge - at a configurable point along the route.

If the pair isn’t registered, the route raises. So PDK authors decide which cross-layer routes are sanctioned, and end users get either a clean transition or an actionable error.

Registering a transition

"""Registering a layer transition.

When a route's port_a and port_b live on different layers, the router
consults a `TransitionRegistry`. If the layer pair has a registered
factory, the router drops in the resulting PCell automatically. Here
we wire SILC ↔ SILN with the built-in `PhotonicElevator`.
"""
import lumicron as lm
import lumicron_pdk as lpdk

LAYER = lpdk.LayerTable(
    SILC = lpdk.Layer(9, 0, color="#008000", min_width=0.2, min_radius=5.0),
    SILN = lpdk.Layer(5, 0, color="#ff69b4", min_width=0.4, min_radius=10.0),
    OXOP = lpdk.Layer(17, 0, color="#d4a574"),
)


@lpdk.route_profile(port_width=0.5, radius=10.0, radius_min=5.0)
def silc_strip(p):
    p.layer(LAYER.SILC, width=p.port_width)


@lpdk.route_profile(port_width=0.5, radius=10.0, radius_min=5.0)
def siln_strip(p):
    p.layer(LAYER.SILN, width=p.port_width)


def silc_siln_elevator():
    """Factory: returns a fresh SILC ↔ SILN elevator each call.

    The router calls this every time a route needs the transition,
    so the registry stores the function rather than a cached cell.
    """
    return lpdk.PhotonicElevator(
        layer_a=LAYER.SILC, layer_b=LAYER.SILN,
        width_a=0.5, width_b=0.5,
        tip_a=0.08, tip_b=0.08,
        taper_length_a=25.0, taper_length_b=25.0,
        overlap=10.0,
        cladding_layer=LAYER.OXOP, cladding_margin=2.0,
        route_profile_a=silc_strip,
        route_profile_b=siln_strip,
    )


# Register both into a PDK-local registry and into the process-wide
# default registry, so end-user scripts get auto-transitions just by
# importing the PDK.
TRANSITIONS = lpdk.TransitionRegistry()
TRANSITIONS.register(LAYER.SILC, LAYER.SILN, silc_siln_elevator)
lpdk.default_transitions().register(LAYER.SILC, LAYER.SILN, silc_siln_elevator)


@lm.pcell
def TransitionDemo():
    """A route whose endpoints are on different layers — the registered
    elevator drops in automatically."""
    c = lm.CELL("TransitionDemo")
    c.add(lm.PORT("in",  position=(0, 0),   direction=0,
                  route_profile=silc_strip))
    c.add(lm.PORT("out", position=(600, 0), direction=180,
                  route_profile=siln_strip))
    c.Route(c.ports["in"], c.ports["out"], transition_offset=300)
    return c


if __name__ == "__main__":
    TransitionDemo().to_gds("transition_demo.gds")
Output of 08_transition_pcell.py - auto-inserted SiLC-to-SiLN elevator.

Two registries appear:

TRANSITIONS = lpdk.TransitionRegistry()                  # PDK-local
TRANSITIONS.register(LAYER.SILC, LAYER.SILN, factory)

lpdk.default_transitions().register(LAYER.SILC, LAYER.SILN, factory)  # process-wide
  • The PDK-local registry is what pdk.TRANSITIONS exposes for users who want to pass it explicitly: chip.Route(a, b, transitions=pdk.TRANSITIONS).
  • The process-wide registry is read when no explicit transitions= is passed. Registering into it from your PDK module - typically inside the transitions.py module body - means importing the PDK is enough to wire up auto-transitions everywhere.

The factory contract

A transition entry maps (layer_a, layer_b) to a factory function, not a pre-built cell:

def silc_siln_elevator():
    return lpdk.PhotonicElevator(...)

The router calls the factory each time a route needs the transition. Why a function rather than a cached cell:

  • The router can pass position-derived kwargs in the future (e.g. mid-bend transition would need a curved entry).
  • Multiple routes through the same transition each get their own SRef of a fresh PCell instance, so per-instance overrides (cladding margin variants) stay clean.
  • It side-steps Python’s import-time evaluation: the factory body can reference layers and profiles defined in any order.

In practice the factory is a one-liner that constructs and returns a PCell. The optional lpdk.PhotonicElevator provides two inverse tapers with an overlap region; use it only when that construction fits the process. A custom CELL with the required ports can implement different transition physics.

What a transition cell needs

The router’s contract with a transition cell is minimal:

  • One input port named i1 on layer_a (matching the route’s incoming layer).
  • One output port named o1 on layer_b (matching the route’s outgoing layer).
  • Both ports should carry a route_profile= so the route’s continuation inherits the right cross-section.

Anything else - internal taper geometry, cladding patterns, doping rails - is up to the cell.

Where transitions appear in a route

The router places the transition at a position controlled by the route call:

chip.Route(a, b, transition_offset=300)

transition_offset=N places the transition’s entry N µm from port_a, along that port’s outgoing direction. The default is 50.0 µm; it is not a midpoint calculation.

For section-level layer changes (.style(profile=siln_strip) mid-route), the router inserts a transition at the section boundary using the same registry. So one registration covers all uses - full-route changes and mid-route changes both.

When the registry doesn’t have an entry

RouteError: no transition registered for layer pair SILC -> MTL1
  Register a factory with TransitionRegistry.register(layer_a, layer_b, factory),
    or stay on a single layer.

This is intentional. Photonic-to-electrical transitions, or photonic transitions across very different stacks (silicon-to-III-V), aren’t well-defined - they need a designed PCell that the foundry has characterized. Forcing PDK authors to register transitions surfaces those design decisions instead of letting the router silently butt-join.

Tip

register(SILC, SILN, factory, symmetric=True) registers both directions; True is the default. The reverse entry uses the same factory with reversed placement. Pass symmetric=False when only the declared direction is supported. Registration authorizes geometry placement; it does not establish reciprocal optical performance.

What ships with the API

Two transition primitives are bundled:

  • lpdk.PhotonicElevator: optional generic inverse-taper geometry for a suitable PDK or reference test.
  • lpdk.TransitionRegistry: the registry container itself.

Custom transition PCells (mode-matched bridges, sub-wavelength couplers, polarization rotators) are PDK-specific. They’re just @pcell cells with i1/o1 ports - write them like any other component and register the factory.

Optional generic elevator

lpdk.PhotonicElevator is the existing generic inverse-taper-style convenience PCell. It can provide reference/test geometry or a transition when its construction matches the platform. It does not prescribe every PDK’s transition physics. A PDK may provide a completely custom transition PCell. Lumicron owns transition reasoning; the PDK owns transition physics.

The generic elevator contains polygons, without a certified optical centerline. Its geometry can be exported in an automatic transition route, but the total optical route length remains unavailable. Neither port separation nor the transition’s bounding box supplies the missing path-length evidence.

Search documentation

Type to search all guides and API references.

↑ ↓ to select · Enter to open · Esc to close