Skip to content

Download notebook (.ipynb)

Schematic-Driven Design

kfactory supports a schematic-first design style: you declare what connects to what and where instances are placed, and kfactory builds the physical layout from that declarative description.

This is distinct from the imperative approach (calling connect() on instances inside a @kf.cell function). Schematics are:

  • Serialisable — stored as YAML/JSON, not just in-memory
  • Verifiable — the extracted netlist can be compared against the schematic (LVS)
  • Code-generatable — a schematic can emit a standalone Python function that re-creates the same layout without the schematic machinery

Key types

Class Description
kf.Schematic DBU-coordinate schematic (placement in database units)
kf.DSchematic µm-coordinate schematic (floating-point placement)
kf.KCLayout.schematic_cell Decorator that turns a schematic factory into a cached cell
import kfactory as kf

Setting up a PDK

Schematics work inside a KCLayout (PDK). Cell functions registered on the PDK are looked up by name when create_inst is called — so every component you place must be registered first.

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


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

Registering PDK cells

PDK cells are plain @pdk.cell-decorated functions. Their parameters must be JSON-serialisable (int, float, str, bool) so the schematic can store them.

@pdk.cell
def straight(width: int, length: int) -> kf.KCell:
    """Waveguide straight segment.

    Args:
        width: Width in dbu.
        length: Length in dbu.
    """
    c = pdk.kcell()
    c.shapes(L.WG).insert(kf.kdb.Box(0, -width // 2, length, width // 2))
    c.create_port(
        name="o1",
        width=width,
        trans=kf.kdb.Trans(rot=2, mirrx=False, x=0, y=0),
        layer_info=L.WG,
    )
    c.create_port(
        name="o2",
        width=width,
        trans=kf.kdb.Trans(x=length, y=0),
        layer_info=L.WG,
    )
    return c


@pdk.cell
def bend90(width: int, radius: int) -> kf.KCell:
    """90° Euler bend.

    Args:
        width: Width in dbu.
        radius: Nominal bend radius in dbu.
    """
    return kf.factories.euler.bend_euler_factory(kcl=pdk)(
        width=pdk.to_um(width),
        radius=pdk.to_um(radius),
        layer=L.WG,
    )

Basic schematic: placement only

The simplest schematic just places instances at known coordinates. schematic.create_inst looks up the component by name in the PDK and records the settings. inst.place(x, y) sets the origin in dbu.

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

    s1 = schematic.create_inst(
        name="s1",
        component="straight",
        settings={"width": 500, "length": 10_000},
    )
    s2 = schematic.create_inst(
        name="s2",
        component="straight",
        settings={"width": 500, "length": 10_000},
    )

    s1.place(x=0, y=0)
    s2.place(x=20_000, y=0)

    return schematic


cell = two_straights()
cell


png

The @pdk.schematic_cell decorator caches the result just like @pdk.cell, so calling two_straights() a second time returns the same object.

Connectivity: connect

inst.connect(port_name, other_port) aligns an instance so that the named port is mated with other_port. This is the schematic equivalent of the imperative instance.connect() in a regular cell body.

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

    s1 = schematic.create_inst(
        name="s1",
        component="straight",
        settings={"width": 500, "length": 20_000},
    )
    b1 = schematic.create_inst(
        name="b1",
        component="bend90",
        settings={"width": 500, "radius": 10_000},
    )
    s2 = schematic.create_inst(
        name="s2",
        component="straight",
        settings={"width": 500, "length": 20_000},
    )

    # Fix s1 at the origin
    s1.place(x=0, y=0)

    # Snap b1's input port ("o1") onto s1's output port ("o2")
    b1.connect("o1", s1.ports["o2"])

    # Snap s2's input port ("o1") onto b1's output port ("o2")
    s2.connect("o1", b1.ports["o2"])

    return schematic


chain = chain_of_three()
chain


png

The schematic model

The schematic attribute on a schematic cell contains the full declarative description as a Pydantic model. This includes instances, placements, nets, and routes.

model = chain.schematic
print("Instances:", list(model.instances.keys()))
print(
    "Placements:", {k: (v.x, v.y, v.orientation) for k, v in model.placements.items()}
)
Instances: ['s1', 'b1', 's2']
Placements: {'s1': (0, 0, 0.0)}

Netlist extraction

cell.netlist() extracts a connectivity netlist from the physical layout. Because each SchematicInstance places a real KCell into the layout, the extracted netlist reflects the actual geometry — not just the schematic intent.

netlist = chain.netlist()
for cell_name, net in netlist.items():
    print(f"\n=== {cell_name} ===")
    print(f"  instances: {list(net.instances.keys())}")
    for i, n in enumerate(net.nets):
        print(f"  net[{i}]: {[f'{p.instance}.{p.port}' for p in n]}")
    print(f"  ports: {[p.name for p in net.ports]}")
=== chain_of_three ===
  instances: ['b1', 's1', 's2']
  net[0]: ['b1.o1', 's1.o2']
  net[1]: ['b1.o2', 's2.o1']
  ports: []

=== bend90_W500_R10000 ===
  instances: []
  ports: ['o1', 'o2']

=== straight_W500_L20000 ===
  instances: []
  ports: ['o1', 'o2']

Schematic netlist vs extracted netlist (LVS)

For a schematic cell with declared nets (schematic.nets), kfactory can compare the schematic connectivity against the extracted layout connectivity. Here we use a version with explicit nets to demonstrate the LVS flow.

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

    s1 = schematic.create_inst(
        name="s1",
        component="straight",
        settings={"width": 500, "length": 15_000},
    )
    s2 = schematic.create_inst(
        name="s2",
        component="straight",
        settings={"width": 500, "length": 15_000},
    )

    s1.place(x=0, y=0)

    # connect() both places s2 and records the connectivity in the schematic model
    s2.connect("o1", s1.ports["o2"])

    return schematic


lvs_cell = lvs_example()

# schematic.netlist() derives connectivity from the declared placements/connections
schematic_netlist = lvs_cell.schematic.netlist()

# cell.netlist() derives connectivity from the physical layout geometry
extracted_netlist = lvs_cell.netlist()[lvs_cell.name]

assert schematic_netlist == extracted_netlist, "LVS failed!"
print("LVS passed: schematic and extracted netlists match.")
LVS passed: schematic and extracted netlists match.

Code generation

schematic.code_str() generates a standalone Python function that re-creates the same layout without the schematic machinery. This is useful for:

  • Exporting a schematic-designed cell for use in a lower-level flow
  • Archiving a point-in-time snapshot of a parameterised design
  • Sharing a self-contained design with collaborators who do not use schematics
from IPython.display import Code

generated = chain.schematic.code_str()
Code(generated)
import kfactory as kf


kcl = kf.kcls["SCHEM_OVERVIEW"]


@kcl.schematic_cell(output_type=kf.DKCell)
def chain_of_three() -> kf.Schematic:
    schematic = kf.Schematic(kcl=kcl)

    # Create the schematic instances
    b1 = schematic.create_inst(
        name="b1",
        component="bend90",
        settings={"width": 500, "radius": 10000},
    )
    s1 = schematic.create_inst(
        name="s1",
        component="straight",
        settings={"width": 500, "length": 20000},
    )
    s2 = schematic.create_inst(
        name="s2",
        component="straight",
        settings={"width": 500, "length": 20000},
    )

    # Schematic instance placements
    s1.place()

    # Schematic connections
    b1.connect(
        port="o1",
        other=s1["o2"],
    )
    b1.connect(
        port="o2",
        other=s2["o1"],
    )
    return schematic

The generated code is a regular @kcl.schematic_cell-decorated function — you can copy it into any file that imports the same PDK and it will produce an identical cell.

Summary

Operation API
Define a schematic cell @pdk.schematic_cell
Add a component instance schematic.create_inst(name, component, settings)
Place at coordinate inst.place(x, y)
Connect two ports inst.connect(port, other_port)
Declare explicit net schematic.add_net(name, [port, ...])
Extract layout netlist cell.netlist()
Get schematic model cell.schematic
Generate standalone code cell.schematic.code_str()

See Also

Topic Where
Netlist data model Schematics: Netlist
45° crossing with virtual cells Schematics: 45° Crossing
Creating a full PDK PDK: Creating a PDK
PCells & caching Components: PCells