Skip to content

Download notebook (.ipynb)

Schematic Ports & Pins

A schematic exposes connectivity to the outside world through two complementary objects:

  • Ports — individual terminals (single waveguide port, single electrical contact). They get materialized as KCell.ports on the resulting cell.
  • Pinsnamed bundles of ports. They group one or more cell-level ports under a single logical terminal (e.g. a DC pad's two contacts, a multi-mode bus).

This tutorial covers both APIs end-to-end: how to expose ports at the schematic level, how to pin them together, how the schematic stores everything as Pydantic data, and how the YAML round-trip works.

For schematic basics (instances, placements, connections) see Schematic-Driven Design.

import kfactory as kf

PDK setup

A minimal PDK with a single straight cell. The cell exposes two waveguide ports (o1, o2) and groups them under one cell-level pin called "dc".

class LAYER(kf.LayerInfos):
    WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)


L = LAYER()
pdk = kf.KCLayout("SCHEM_PORTS_PINS", infos=LAYER)


@pdk.cell
def straight(width: int = 500, length: int = 10_000) -> kf.KCell:
    """Straight waveguide. Both optical ports are also grouped under a `"dc"` pin."""
    c = pdk.kcell()
    c.shapes(L.WG).insert(kf.kdb.Box(0, -width // 2, length, width // 2))
    p1 = c.create_port(
        name="o1",
        width=width,
        trans=kf.kdb.Trans(rot=2, x=0, y=0),
        layer_info=L.WG,
    )
    p2 = c.create_port(
        name="o2",
        width=width,
        trans=kf.kdb.Trans(x=length, y=0),
        layer_info=L.WG,
    )
    c.create_pin(name="dc", ports=[p1, p2], pin_type="DC", info={"role": "bus"})
    return c

Part 1 — Ports on a schematic

A KCell exposes ports via c.create_port(...). A schematic exposes ports via schematic.ports — a dict mapping name → one of three things:

Stored as Created with Meaning
PortRef / PortArrayRef schematic.add_port(name, port=inst.ports[...]) Forward an instance port up to the top level
Port[T] schematic.create_port(name, cross_section, x, y, ...) A new placeable top-level port whose position is computed at build time

Schematic ports are materialized as KCell.ports when the cell is built. For PortRef, the underlying instance port is added to the top-level cell under the schematic-port key name.

add_port — forward an instance port

Use add_port whenever you want a top-level port that simply mirrors an existing instance port. The schematic stores a PortRef lazily; the cell port is created when create_cell runs.

@pdk.schematic_cell
def forwarded_ports() -> kf.Schematic:
    schematic = kf.Schematic(kcl=pdk)

    s = schematic.create_inst(
        name="s", component="straight", settings={"width": 500, "length": 10_000}
    )
    s.place(x=0, y=0)

    # forward each instance port up to the top level. the schematic-port key name
    # becomes the cell-port name on the resulting KCell — it does not have to match
    # the instance's port name.
    schematic.add_port(name="left", port=s.ports["o1"])
    schematic.add_port(name="right", port=s.ports["o2"])

    return schematic


cell = forwarded_ports()
print("cell ports:", [p.name for p in cell.ports])
cell ports: ['right', 'left']

Inside the schematic, the entries are PortRef objects pointing at the underlying instance:

for name, port in cell.schematic.ports.items():
    print(f"  {name!r}: {port}")
  'left': s['o1']
  'right': s['o2']

create_port — placeable top-level port

Use create_port when the top-level port is not a copy of an instance port — typically when you want a port at a fixed location, or whose position/orientation is a function of an instance's geometry but not directly any port.

Both absolute coordinates and PortRefs are accepted for x, y, and orientation, so you can pin the new port to an instance:

@pdk.schematic_cell
def fanout_port() -> kf.Schematic:
    schematic = kf.Schematic(kcl=pdk)

    s = schematic.create_inst(
        name="s", component="straight", settings={"width": 500, "length": 10_000}
    )
    s.place(x=0, y=0)

    # an extra "monitor" port placed 5 µm above the centre of the straight, facing up
    schematic.create_port(
        name="monitor",
        cross_section="WG_500",
        x=5_000,
        y=5_000,
        orientation=90,
    )

    schematic.add_port(name="left", port=s.ports["o1"])
    schematic.add_port(name="right", port=s.ports["o2"])
    return schematic

Note

create_port requires a registered cross-section. The example above assumes a WG_500 cross-section was registered on the PDK; in this notebook we don't instantiate fanout_port() because the PDK has no cross-section table — it's here for API illustration only. See the PDK page for setting up cross-sections.

Part 2 — Pins

A pin is a named bundle of ports. It carries a pin_type ("DC" by default) and free-form info. At the schematic level there are two ways to define one:

Stored as Created with Meaning
Pin schematic.create_pin(name, ports=[...]) Group existing top-level schematic ports
PinRef schematic.add_pin(name, pin=inst.pins["..."]) Forward an instance's pin to the top level

Pins are structural only for now — they're not part of nets, connections, or routes. They sit alongside ports on the schematic and on the resulting cell.

create_pin — explicit grouping

Use this when the pin's member ports come from different instances or don't align with any single instance pin. The constituent port names must already exist in schematic.ports (typically via add_port).

@pdk.schematic_cell
def explicit_pin() -> kf.Schematic:
    schematic = kf.Schematic(kcl=pdk)

    s = schematic.create_inst(
        name="s", component="straight", settings={"width": 500, "length": 10_000}
    )
    s.place(x=0, y=0)

    schematic.add_port(name="left", port=s.ports["o1"])
    schematic.add_port(name="right", port=s.ports["o2"])

    schematic.create_pin(
        name="bus", ports=["left", "right"], pin_type="RF", info={"freq": 5}
    )

    return schematic


cell = explicit_pin()
for pin in cell.pins:
    print(
        f"pin {pin.name!r}:"
        f" ports={[p.name for p in pin.ports]}"
        f" type={pin.pin_type!r}"
        f" info={dict(pin.info)}"
    )
pin 'bus': ports=['left', 'right'] type='RF' info={'freq': 5}

add_pin — forward an instance pin

Use add_pin when an instance already exposes a pin you want at the top level. inst.pins["name"] produces a PinRef; pass it to add_pin.

Pre-condition: every constituent port of the instance pin must be exposed as a top-level schematic port beforehand. The materialization step at create_cell time looks up each port via that exposure — if any port is missing you'll get a clear error.

@pdk.schematic_cell
def forwarded_pin() -> kf.Schematic:
    schematic = kf.Schematic(kcl=pdk)

    s = schematic.create_inst(
        name="s", component="straight", settings={"width": 500, "length": 10_000}
    )
    s.place(x=0, y=0)

    # both underlying ports of the instance's "dc" pin are needed at the top level
    schematic.add_port(name="left", port=s.ports["o1"])
    schematic.add_port(name="right", port=s.ports["o2"])

    # forward the pin — pin_type and info are inherited from the instance pin
    schematic.add_pin(name="dc", pin=s.pins["dc"])
    return schematic


cell = forwarded_pin()
for pin in cell.pins:
    print(f"pin {pin.name!r}: type={pin.pin_type!r} info={dict(pin.info)}")
pin 'dc': type='DC' info={'role': 'bus'}

Part 3 — YAML round-trip

Schematic ports and pins serialize alongside instances and placements. The format accepts two shorthands:

  • Ports: "<inst>,<port>" (forwarded) or a {x, y, ...} dict (placeable)
  • Pins: "<inst>,<pin>" (forwarded) or a {ports: [...], pin_type, info} block
yaml_str = """
instances:
  s:
    component: straight
    settings: {width: 500, length: 10000}

placements:
  s: {x: 0, y: 0}

ports:
  left:  s,o1
  right: s,o2

pins:
  bus:
    ports: [left, right]
    pin_type: RF
  fwd: s,dc
"""

from ruamel.yaml import YAML

yaml = YAML(typ="safe")
schematic = kf.Schematic.model_validate(yaml.load(yaml_str))

print("ports:", {n: type(p).__name__ for n, p in schematic.ports.items()})
print("pins:", {n: type(p).__name__ for n, p in schematic.pins.items()})
print("bus.ports:", schematic.pins["bus"].ports)
print("fwd:", schematic.pins["fwd"])
ports: {'left': 'PortRef', 'right': 'PortRef'}
pins: {'bus': 'Pin', 'fwd': 'PinRef'}
bus.ports: ['left', 'right']
fwd: instance='s' pin='dc'

Pitfalls

  • Schematic-port key ≠ original port name. When you forward an instance port with add_port(name="left", port=s.ports["o1"]), the cell's top-level port is named "left", not "o1". The same naming applies to ports referenced from pins.
  • Forwarded pins need exposed underlying ports. When you call add_pin(pin=inst.pins["x"]), every constituent port of the instance pin must already be a top-level schematic port. The materialization step at create_cell time raises a clear error otherwise.
  • Pins are top-level only. They don't take part in nets, connections, or routes. Connect ports the usual way (inst.connect, add_route); pins exist purely to group ports for tooling that consumes them.
  • Names are unique within their dict. add_port/create_port share schematic.ports; add_pin/create_pin share schematic.pins. Each raises on duplicate names.

Summary

Operation API
Forward an instance port to the top level schematic.add_port(name, port=inst.ports[...])
Create a placeable top-level port schematic.create_port(name, cross_section, x, y, orientation)
Reference an instance port inst.ports["o1"]PortRef
Reference an array instance port inst.ports["o1", ia, ib]PortArrayRef
Group existing top-level ports into a pin schematic.create_pin(name, ports=[...], pin_type, info)
Forward an instance pin to the top level schematic.add_pin(name, pin=inst.pins[...])
Reference an instance pin inst.pins["dc"]PinRef
Inspect cell-level ports / pins cell.ports, cell.pins