# Netlist &amp; Schematic I/O

This page dives into the **Netlist data model** — the representation that kfactory uses for connectivity — and shows how to:

- Inspect the `Netlist` object (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](<https://gdsfactory.github.io/kfactory/3.5.0/schematics/overview/index.md>) 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 (4):
  net[0]: b1.o1 — s1.o2
  net[1]: b1.o2 — s2.o1
  net[2]: s1.o1
  net[3]: s2.o2

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:587: 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"`).

Use `Schematic.from_pic_yml` to load a `pic.yml`-style dictionary (it sanitizes legacy keys before validation), or use the convenience `read_schematic` helper. Plain `model_validate` expects the internal schema and will not translate `pic.yml`-specific fields like `connections` or `port`-based placements.

```
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"}],[{"instance":"x1","port":"o2"}],[{"instance":"x1","port":"o3"}],[{"instance":"x1","port":"o4"}],[{"instance":"x2","port":"o1"}],[{"instance":"x2","port":"o2"}],[{"instance":"x2","port":"o4"}]],"ports":[]}
```

The JSON contains three top-level keys:

- `instances` — each instance's `component`, `kcl` (the owning KCLayout name), and `settings` (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 because `crossing_demo` doesn'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 &amp; connections | [Schematics: Overview](<https://gdsfactory.github.io/kfactory/3.5.0/schematics/overview/index.md>) |
| 45° crossing with virtual cells | [Schematics: 45° Crossing](<https://gdsfactory.github.io/kfactory/3.5.0/schematics/crossing45/index.md>) |
| Creating a full PDK | [PDK: Creating a PDK](<https://gdsfactory.github.io/kfactory/3.5.0/pdk/creating_pdk/index.md>) |
