Netlist & Schematic I/O
This page dives into the Netlist data model — the representation that kfactory uses for connectivity — and shows how to:
- Inspect the
Netlistobject (instances, nets, ports) - Serialize a schematic to JSON/YAML and reload it with
kf.read_schematic - Compare netlists and sort them for stable equality checks
- Dump a netlist directly to JSON via
Netlist.to_json
For the schematic-first design workflow itself (placement, connect, LVS basics) see
the Schematic-Driven Design page.
import json
import tempfile
from pathlib import Path
from kfnetlist import PortRef
import kfactory as kf
PDK setup
We reuse the same minimal PDK from the overview page — a straight waveguide and a 90 °
euler bend, both registered on a dedicated KCLayout.
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_NETLIST", infos=LAYER)
@pdk.cell
def straight(width: int, length: int) -> kf.KCell:
"""Straight waveguide 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,
)
Building a schematic for inspection
A simple L-shaped path: s1 → b1 → s2.
@pdk.schematic_cell
def l_path() -> kf.Schematic:
schematic = kf.Schematic(kcl=pdk)
s1 = schematic.create_inst("s1", "straight", {"width": 500, "length": 15_000})
b1 = schematic.create_inst("b1", "bend90", {"width": 500, "radius": 10_000})
s2 = schematic.create_inst("s2", "straight", {"width": 500, "length": 15_000})
s1.place(x=0, y=0)
b1.connect("o1", s1.ports["o2"])
s2.connect("o1", b1.ports["o2"])
return schematic
cell = l_path()
cell
The Netlist data model
KCell.netlist() returns a dict[str, Netlist] keyed by cell name. Each Netlist
has three attributes:
| Attribute | Type | Meaning |
|---|---|---|
instances |
dict[str, NetlistInstance] |
Every sub-cell placed in this cell |
nets |
list[Net] |
Each net is a list of PortRef / NetlistPort entries that are connected |
ports |
list[NetlistPort] |
Top-level ports exposed by this cell |
A PortRef identifies a port by instance name and port name: PortRef(instance="s1", port="o2").
A NetlistPort identifies a cell-level port by name.
netlists = cell.netlist()
nl = netlists[cell.name].normalize()
print(f"Instances ({len(nl.instances)}):")
for name, inst in nl.instances.items():
print(f" {name}: component={inst.component!r} settings={inst.settings}")
print(f"\nNets ({len(nl.nets)}):")
for i, net in enumerate(nl.nets):
parts = []
for p in net:
if isinstance(p, PortRef):
parts.append(f"{p.instance}.{p.port}")
else:
parts.append(f"<cell-port:{p.name}>")
print(f" net[{i}]: {' — '.join(parts)}")
print(f"\nTop-level ports ({len(nl.ports)}): {[p.name for p in nl.ports]}")
Instances (3):
b1: component='bend90' settings={'radius': 10000, 'width': 500}
s1: component='straight' settings={'length': 15000, 'width': 500}
s2: component='straight' settings={'length': 15000, 'width': 500}
Nets (2):
net[0]: b1.o1 — s1.o2
net[1]: b1.o2 — s2.o1
Top-level ports (0): []
Sorting nets for stable comparison
Port ordering within a net and the order of nets across the netlist can vary between
runs. Netlist.sort() normalises both, making equality checks reproducible.
nl_a = cell.netlist()[cell.name]
nl_b = cell.netlist()[cell.name]
# sort() modifies in-place and returns self
nl_a.sort()
nl_b.sort()
assert nl_a == nl_b
print("Sorted netlists are equal ✓")
Sorted netlists are equal ✓
Schematic serialization
A Schematic is a Pydantic model, so standard Pydantic serialization methods work
directly.
JSON export
model = cell.schematic
raw_json = model.model_dump_json(indent=2)
data = json.loads(raw_json)
# Show the top-level keys present in the serialized schematic
print("Top-level keys:", list(data.keys()))
# Show the placements section
print("\nPlacements:")
for name, placement in data.get("placements", {}).items():
print(f" {name}: {placement}")
Top-level keys: ['name', 'instances', 'placements', 'nets', 'routes', 'ports', 'pins', 'constraints', 'unit', 'info']
Placements:
s1: {'mirror': False, 'x': 0, 'dx': 0, 'y': 0, 'dy': 0, 'orientation': 0.0, 'anchor': None}
/home/runner/work/kfactory/kfactory/.venv/lib/python3.14/site-packages/pydantic/main.py:542: UserWarning: Pydantic serializer warnings:
PydanticSerializationUnexpectedValue(Expected `RouteNet[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s1', port='o2'))), input_type=Connection[TypeVar]])
PydanticSerializationUnexpectedValue(Expected `Connection[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s1', port='o2'))), input_type=Connection[TypeVar]])
PydanticSerializationUnexpectedValue(Expected `VirtualConnection[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s1', port='o2'))), input_type=Connection[TypeVar]])
PydanticSerializationUnexpectedValue(Expected `RouteNet[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s2', port='o1'))), input_type=Connection[TypeVar]])
PydanticSerializationUnexpectedValue(Expected `Connection[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s2', port='o1'))), input_type=Connection[TypeVar]])
PydanticSerializationUnexpectedValue(Expected `VirtualConnection[Annotated[int, str]]` - serialized value may not be as expected [field_name='nets', input_value=Connection[TypeVar](net=(...tance='s2', port='o1'))), input_type=Connection[TypeVar]])
return self.__pydantic_serializer__.to_json(
YAML round-trip with read_schematic
Schematics are typically stored as YAML files for version control. kf.read_schematic
loads them back into a Schematic (or DSchematic when unit="um").
Schematic is also a Pydantic model, so model_validate works directly with a
dictionary from yaml.safe_load — or you can use the convenience read_schematic helper.
with tempfile.TemporaryDirectory() as tmpdir:
yaml_path = Path(tmpdir) / "l_path.yaml"
# Exclude the 'unit' field — it is fixed by the Schematic subclass and must not
# be present in the serialized payload for read_schematic to accept it.
yaml_path.write_text(model.model_dump_json(indent=2, exclude={"unit"}))
# Read back. NOTE: a known upstream bug currently prevents the JSON
# produced by `model_dump_json` from round-tripping through
# `read_schematic` when nets are present (see kfactory issue tracker —
# the `nets` validator expects `{"p1": ..., "p2": ...}` keys but
# `model_dump_json` serialises them as nested arrays). The pattern is
# shown for documentation; uncomment to test once the upstream fix lands.
try:
reloaded = kf.read_schematic(yaml_path, unit="dbu")
print("Reloaded schematic instances:", list(reloaded.instances.keys()))
print("Reloaded schematic placements:", list(reloaded.placements.keys()))
except KeyError as exc:
print(f"(known upstream round-trip bug — KeyError: {exc})")
(known upstream round-trip bug — KeyError: 'p1')
The reloaded schematic carries the same instance definitions and placements.
Calling reloaded.create_cell(output_type=kf.KCell, factories={"straight": straight, "bend90": bend90})
would materialise an identical physical cell.
Netlist as JSON
Netlist.to_json() serialises a netlist directly to a JSON string. This is
the canonical wire format for handing a netlist to external tools or storing
it in a regression-test fixture.
Below we build a small layout with the smallest available crossing
primitive — a cross cell — connected to a second crossing through a
straight waveguide. We annotate each instance with a text label so that the
rendered layout matches the names that appear in the JSON output.
@pdk.cell
def cross(width: int, length: int) -> kf.KCell:
"""Minimal 4-port waveguide crossing.
Two perpendicular rectangles meeting at the origin. Real PDK crossings
use tapered arms and an enclosure (see the `crossing45.py` tutorial) but
this minimal version is sufficient for demonstrating the netlist format.
Args:
width: Waveguide width in dbu.
length: Arm length in dbu (also the cell footprint).
"""
c = pdk.kcell()
c.shapes(L.WG).insert(
kf.kdb.Box(-length // 2, -width // 2, length // 2, width // 2)
)
c.shapes(L.WG).insert(
kf.kdb.Box(-width // 2, -length // 2, width // 2, length // 2)
)
c.create_port(
name="o1",
width=width,
trans=kf.kdb.Trans(rot=0, mirrx=False, x=length // 2, y=0),
layer_info=L.WG,
)
c.create_port(
name="o2",
width=width,
trans=kf.kdb.Trans(rot=1, mirrx=False, x=0, y=length // 2),
layer_info=L.WG,
)
c.create_port(
name="o3",
width=width,
trans=kf.kdb.Trans(rot=2, mirrx=False, x=-length // 2, y=0),
layer_info=L.WG,
)
c.create_port(
name="o4",
width=width,
trans=kf.kdb.Trans(rot=3, mirrx=False, x=0, y=-length // 2),
layer_info=L.WG,
)
return c
@pdk.cell
def crossing_demo() -> kf.KCell:
"""Two crossings connected by a straight, with instance-name labels."""
c = pdk.kcell()
x1 = c.create_inst(cross(width=500, length=10_000))
x1.name = "x1"
s_inst = c.create_inst(straight(width=500, length=15_000))
s_inst.name = "s1"
s_inst.connect("o1", x1.ports["o1"])
x2_inst = c.create_inst(cross(width=500, length=10_000))
x2_inst.name = "x2"
x2_inst.connect("o3", s_inst.ports["o2"])
# Annotate each instance with its name so the rendered layout matches the
# JSON output below.
for inst in c.insts:
center = inst.ibbox().center()
c.shapes(c.kcl.layer(L.WGEX)).insert(
kf.kdb.Text(inst.name, kf.kdb.Trans(center.x, center.y))
)
return c
crossing_cell = crossing_demo()
crossing_cell
Extract the netlist and dump it to JSON. Calling sort() first keeps the
instance ordering stable so the JSON output is reproducible.
nl = crossing_cell.netlist()[crossing_cell.name]
nl.sort()
print(nl.to_json())
{"instances":{"s1":{"kcl":"SCHEM_NETLIST","component":"straight","settings":{"length":15000,"width":500}},"x1":{"kcl":"SCHEM_NETLIST","component":"cross","settings":{"length":10000,"width":500}},"x2":{"kcl":"SCHEM_NETLIST","component":"cross","settings":{"length":10000,"width":500}}},"nets":[[{"instance":"s1","port":"o1"},{"instance":"x1","port":"o1"}],[{"instance":"s1","port":"o2"},{"instance":"x2","port":"o3"}]],"ports":[]}
The JSON contains three top-level keys:
instances— each instance'scomponent,kcl(the owning KCLayout name), andsettings(the constructor kwargs).nets— each net is a list of{"instance": ..., "port": ...}entries that are electrically tied together.ports— the top-level cell-exposed ports (empty here becausecrossing_demodoesn't expose any ports).
Building a Netlist programmatically
You can also construct a Netlist directly without going through a schematic — useful
for testing or for importing connectivity from an external source.
from kfactory import Netlist
manual_nl = Netlist()
# Add instance definitions
manual_nl.create_inst(
"wg1", kcl="MY_PDK", component="straight", settings={"width": 500, "length": 10_000}
)
manual_nl.create_inst(
"wg2", kcl="MY_PDK", component="straight", settings={"width": 500, "length": 10_000}
)
# Add a top-level port
p_in = manual_nl.create_port("in")
# Connect: cell-level "in" is tied to wg1.o1
manual_nl.create_net(p_in, PortRef(instance="wg1", port="o1"))
# Internal net: wg1.o2 connects to wg2.o1
manual_nl.create_net(
PortRef(instance="wg1", port="o2"),
PortRef(instance="wg2", port="o1"),
)
manual_nl.sort()
print("Instances:", list(manual_nl.instances.keys()))
print("Top-level ports:", [p.name for p in manual_nl.ports])
print("Nets:")
for i, net in enumerate(manual_nl.nets):
parts = [
f"{p.instance}.{p.port}" if isinstance(p, PortRef) else f"<{p.name}>"
for p in net
]
print(f" net[{i}]: {parts}")
Instances: ['wg1', 'wg2']
Top-level ports: ['in']
Nets:
net[0]: ['<in>', 'wg1.o1']
net[1]: ['wg1.o2', 'wg2.o1']
Summary
| Task | API |
|---|---|
| Extract netlist from a cell | cell.netlist() → dict[str, Netlist] |
| Iterate nets | for net in nl.nets: for p in net: ... |
| Sort for stable comparison | nl.sort() |
| Export schematic to JSON | schematic.model_dump_json() |
| Load schematic from YAML/JSON file | kf.read_schematic(path, unit="dbu") |
| Dump a netlist to JSON | nl.to_json() |
| Build a netlist without a schematic | Netlist(); nl.create_inst(...); nl.create_net(...) |
See Also
| Topic | Where |
|---|---|
| Schematic placement & connections | Schematics: Overview |
| 45° crossing with virtual cells | Schematics: 45° Crossing |
| Creating a full PDK | PDK: Creating a PDK |