Skip to content

Download notebook (.ipynb)

Connectivity Verification (LVS)

Layout vs. Schematic (LVS) verification confirms that the physical layout of a circuit matches its intended connectivity. In photonic and electronic design, the schematic defines the intended connections between components, while the extracted netlist captures what was actually built in the layout.

kfnetlist provides the building blocks for a complete LVS workflow: detect opens, compare against a reference, and find geometric shorts. This guide walks through each step and shows how to interpret the results.

Verification methods

Method LVS category What it detects Requires klayout?
detect_opens() Open circuit Unconnected ports, singleton nets No
find_net_difference(ref) Net mismatch Missing / extra nets vs. reference No
detect_shorts(l2n) Short circuit Geometric polygon overlaps between nets Yes
normalize() Pre-processing Folds equivalent ports before comparison No
check_connection() Port validation Geometric port-pair alignment checks Yes

Step 1 — Build the netlists

A typical LVS flow compares two netlists:

  • Schematic netlist — the golden reference (what you intended)
  • Extracted netlist — what was actually built (from layout extraction)

Below we construct both by hand, deliberately introducing errors in the extracted netlist to demonstrate each detection method.

from kfnetlist import Netlist, NetlistPort, PortRef

# -- Schematic (golden reference) --
schematic = Netlist()
schematic.create_inst("mmi", kcl="PDK", component="mmi1x2", settings={"width": 500})
schematic.create_inst("wg1", kcl="PDK", component="straight", settings={"length": 10_000})
schematic.create_inst("wg2", kcl="PDK", component="straight", settings={"length": 10_000})

schematic.create_port("in")
schematic.create_port("out1")
schematic.create_port("out2")

schematic.create_net(NetlistPort(name="in"), PortRef(instance="mmi", port="o1"))
schematic.create_net(PortRef(instance="mmi", port="o2"), PortRef(instance="wg1", port="o1"))
schematic.create_net(PortRef(instance="mmi", port="o3"), PortRef(instance="wg2", port="o1"))
schematic.create_net(PortRef(instance="wg1", port="o2"), NetlistPort(name="out1"))
schematic.create_net(PortRef(instance="wg2", port="o2"), NetlistPort(name="out2"))
schematic.sort()

print(f"Schematic: {len(schematic.nets)} nets, {len(schematic.ports)} ports")
Schematic: 5 nets, 3 ports
# -- Extracted netlist (from layout) --
# Errors introduced:
#   1. The mmi→wg2 connection is missing (open)
#   2. wg2.o1 is left dangling as a singleton net (stub)
#   3. An extra net connects wg2.o2 directly to "in" (unintended)

extracted = Netlist()
extracted.create_inst("mmi", kcl="PDK", component="mmi1x2", settings={"width": 500})
extracted.create_inst("wg1", kcl="PDK", component="straight", settings={"length": 10_000})
extracted.create_inst("wg2", kcl="PDK", component="straight", settings={"length": 10_000})

extracted.create_port("in")
extracted.create_port("out1")
extracted.create_port("out2")

extracted.create_net(NetlistPort(name="in"), PortRef(instance="mmi", port="o1"))
extracted.create_net(PortRef(instance="mmi", port="o2"), PortRef(instance="wg1", port="o1"))
# Missing: mmi.o3 → wg2.o1
extracted.create_net(PortRef(instance="wg2", port="o1"))  # singleton (dangling stub)
extracted.create_net(PortRef(instance="wg1", port="o2"), NetlistPort(name="out1"))
extracted.create_net(PortRef(instance="wg2", port="o2"), NetlistPort(name="out2"))
extracted.sort()

print(f"Extracted: {len(extracted.nets)} nets, {len(extracted.ports)} ports")
Extracted: 5 nets, 3 ports

Step 2 — Pre-process with equivalent ports

Before comparing, fold electrically-equivalent ports into canonical names using normalize(). This prevents false mismatches on components like pads where multiple pins connect to the same metal plane.

In this example neither netlist has equivalent ports, so normalize() is a no-op — but it should always be part of the workflow.

equivalent_ports = {
    # "pad": [["p1", "p2"]],  # uncomment if your design has equivalent port groups
}

schematic_norm = schematic.normalize(cell_name="top", equivalent_ports=equivalent_ports)
extracted_norm = extracted.normalize(cell_name="top", equivalent_ports=equivalent_ports)

schematic_norm.sort()
extracted_norm.sort()
print("Normalization applied")
Normalization applied

Step 3 — Detect opens

detect_opens() inspects a single netlist for signs of incomplete wiring. It returns a dict with two keys:

  • unconnected_ports — top-level ports not referenced by any net
  • singleton_nets — nets with exactly one member (dangling stubs)
opens = extracted_norm.detect_opens()

print("Unconnected ports:", opens["unconnected_ports"])

singleton_nets = list(opens["singleton_nets"])
print(f"Singleton nets: {len(singleton_nets)}")
for net in singleton_nets:
    members = [
        f"{m.instance}.{m.port}" if isinstance(m, PortRef) else f"<{m.name}>"
        for m in net
    ]
    print(f"  dangling: {' — '.join(members)}")
Unconnected ports: []
Singleton nets: 1
  dangling: wg2.o1

Interpreting open detection results

Result LVS meaning Severity Typical cause
Unconnected port Definite open — a declared port is not wired Error Missing route to a top-level pin
Singleton net Potential open — a port is wired but goes nowhere Warning Broken connection, dangling stub

Step 4 — Compare against reference

find_net_difference(reference) compares two netlists by symmetric difference on their nets. It returns:

  • missing — nets in the reference that are absent from the extracted netlist (connections that should exist but don't)
  • extra — nets in the extracted netlist that are absent from the reference (unintended connections)
diff = extracted_norm.find_net_difference(schematic_norm)

missing = list(diff["missing"])
extra = list(diff["extra"])

print(f"Missing nets (in schematic but not in layout): {len(missing)}")
for net in missing:
    members = [
        f"{m.instance}.{m.port}" if isinstance(m, PortRef) else f"<{m.name}>"
        for m in net
    ]
    print(f"  {' — '.join(members)}")

print(f"\nExtra nets (in layout but not in schematic): {len(extra)}")
for net in extra:
    members = [
        f"{m.instance}.{m.port}" if isinstance(m, PortRef) else f"<{m.name}>"
        for m in net
    ]
    print(f"  {' — '.join(members)}")
Missing nets (in schematic but not in layout): 1
  mmi.o3 — wg2.o1

Extra nets (in layout but not in schematic): 1
  wg2.o1

Interpreting net differences

Result LVS meaning Severity Typical cause
Missing net Open — a connection exists in the schematic but not in the layout Error Broken or missing route
Extra net Unintended connection — a connection exists in the layout but not in the schematic Warning Accidental coupling, routing error

Step 5 — Detect geometric shorts (requires klayout)

detect_shorts(l2n) finds polygon overlaps between electrically distinct nets on the same layer. This requires a completed klayout LayoutToNetlist extraction.

from kfnetlist.extract import detect_shorts

shorts = detect_shorts(l2n)

for s in shorts:
    print(f"SHORT: {s.net_a} <-> {s.net_b} on {s.layer}")
    print(f"  Overlap area: {s.overlap.area()} dbu^2")
    print(f"  Bounding box: {s.overlap.bbox()}")

ShortResult fields

Field Type Description
net_a str Name of the first net
net_b str Name of the second net
layer str Layer display name where the overlap occurs
overlap kdb.Region The overlapping polygon region

The overlap region can be used to compute the area of the short (s.overlap.area()), get its bounding box (s.overlap.bbox()), or visualize it in klayout's layout viewer.

Filtering by layer

Use the short_layers parameter to restrict detection to specific layers:

from klayout import db as kdb

# Only check metal layers
shorts = detect_shorts(
    l2n,
    short_layers=[kdb.LayerInfo(1, 0), kdb.LayerInfo(2, 0)],
)

Step 6 — Build a verification summary

Aggregate all findings into a single pass/fail report.

def build_verification_summary(opens_result, diff_result, shorts=None):
    """Aggregate verification results into a structured summary."""
    n_unconnected = len(opens_result["unconnected_ports"])
    n_singletons = len(list(opens_result["singleton_nets"]))
    n_missing = len(list(diff_result["missing"]))
    n_extra = len(list(diff_result["extra"]))
    n_shorts = len(shorts) if shorts else 0

    passed = (n_unconnected == 0 and n_missing == 0 and n_shorts == 0)

    return {
        "pass": passed,
        "unconnected_ports": n_unconnected,
        "singleton_nets": n_singletons,
        "missing_nets": n_missing,
        "extra_nets": n_extra,
        "shorts": n_shorts,
    }


summary = build_verification_summary(opens, diff)

status = "PASS" if summary["pass"] else "FAIL"
print(f"VERIFICATION RESULT: {status}")
print(f"  Opens: {summary['unconnected_ports']} unconnected port(s), "
      f"{summary['singleton_nets']} singleton net(s)")
print(f"  Net differences: {summary['missing_nets']} missing, "
      f"{summary['extra_nets']} extra")
print(f"  Shorts: {summary['shorts']}")
VERIFICATION RESULT: FAIL
  Opens: 0 unconnected port(s), 1 singleton net(s)
  Net differences: 1 missing, 1 extra
  Shorts: 0

Exploring report data

The dicts returned by detect_opens() and find_net_difference() are the primary "report database" in kfnetlist. Here is how to explore them programmatically.

Iterating net members

Each net in the missing or extra lists is a Net object that can be iterated to inspect its members:

for net in list(diff["missing"]):
    port_refs = []
    netlist_ports = []
    for m in net:
        if isinstance(m, PortRef):
            port_refs.append(f"{m.instance}.{m.port}")
        elif isinstance(m, NetlistPort):
            netlist_ports.append(m.name)
    print(f"Missing net: ports={netlist_ports}, refs={port_refs}")
Missing net: ports=[], refs=['mmi.o3', 'wg2.o1']

Classifying findings by category

You can tag each finding with an LVS error category for structured reporting:

findings = []

for port_name in opens["unconnected_ports"]:
    findings.append({
        "category": "open",
        "severity": "error",
        "detail": f"Unconnected top-level port: {port_name}",
    })

for net in list(opens["singleton_nets"]):
    member = next(iter(net))
    label = (f"{member.instance}.{member.port}"
             if isinstance(member, PortRef) else member.name)
    findings.append({
        "category": "open",
        "severity": "warning",
        "detail": f"Singleton net (dangling stub): {label}",
    })

for net in list(diff["missing"]):
    members = [
        f"{m.instance}.{m.port}" if isinstance(m, PortRef) else f"<{m.name}>"
        for m in net
    ]
    findings.append({
        "category": "net_mismatch",
        "severity": "error",
        "detail": f"Missing net: {' — '.join(members)}",
    })

for net in list(diff["extra"]):
    members = [
        f"{m.instance}.{m.port}" if isinstance(m, PortRef) else f"<{m.name}>"
        for m in net
    ]
    findings.append({
        "category": "net_mismatch",
        "severity": "warning",
        "detail": f"Extra net: {' — '.join(members)}",
    })

print(f"Total findings: {len(findings)}")
for f in findings:
    print(f"  [{f['severity'].upper():7s}] {f['category']:15s} | {f['detail']}")
Total findings: 3
  [WARNING] open            | Singleton net (dangling stub): wg2.o1
  [ERROR  ] net_mismatch    | Missing net: mmi.o3 — wg2.o1
  [WARNING] net_mismatch    | Extra net: wg2.o1

Filtering by severity or category

Since findings are plain dicts, standard Python filtering works:

errors_only = [f for f in findings if f["severity"] == "error"]
opens_only = [f for f in findings if f["category"] == "open"]

print(f"Errors: {len(errors_only)}, Opens: {len(opens_only)}")
Errors: 1, Opens: 1

Complete verification workflow

Here is the full flow as a reusable pattern:

from kfnetlist import Netlist, NetlistPort, PortRef

def verify_connectivity(
    extracted: Netlist,
    schematic: Netlist,
    equivalent_ports: dict | None = None,
    l2n=None,
    short_layers=None,
) -> dict:
    """Run all connectivity checks and return a structured report."""
    eq = equivalent_ports or {}

    # 1. Normalize equivalent ports
    ext = extracted.normalize(cell_name="top", equivalent_ports=eq)
    sch = schematic.normalize(cell_name="top", equivalent_ports=eq)
    ext.sort()
    sch.sort()

    # 2. Detect opens in extracted netlist
    opens = ext.detect_opens()

    # 3. Compare against schematic
    diff = ext.find_net_difference(sch)

    # 4. Detect geometric shorts (if L2N available)
    shorts = []
    if l2n is not None:
        from kfnetlist.extract import detect_shorts
        shorts = detect_shorts(l2n, short_layers=short_layers)

    # 5. Build summary
    n_unconnected = len(opens["unconnected_ports"])
    n_missing = len(list(diff["missing"]))
    n_shorts = len(shorts)

    return {
        "pass": n_unconnected == 0 and n_missing == 0 and n_shorts == 0,
        "opens": opens,
        "diff": diff,
        "shorts": shorts,
        "summary": {
            "unconnected_ports": n_unconnected,
            "singleton_nets": len(list(opens["singleton_nets"])),
            "missing_nets": n_missing,
            "extra_nets": len(list(diff["extra"])),
            "shorts": n_shorts,
        },
    }

Mapping to LVS error categories

kfnetlist finding Traditional LVS category Severity Typical cause
Unconnected port Open Error Missing route to a top-level pin
Singleton net Open (potential) Warning Dangling stub, broken connection
Missing net (vs. reference) Net mismatch / open Error Missing or broken route in layout
Extra net (vs. reference) Net mismatch Warning Unintended connection, routing error
ShortResult Short Error Metal overlap between distinct nets
PortCheck failure DRC / port alignment Warning Misaligned or wrong-width port

Future: structured error reporting

kfnetlist is working toward structured error reporting inspired by the elvis LVS engine. Planned capabilities include:

  • Error accumulation — collect all issues into a single result container instead of throwing on first error
  • KLayout RDB output — serialize errors with geometric markers for visualization in klayout's marker browser
  • Error filtering pipeline — deduplicate and merge transitive errors (e.g. collapse a power-rail short from hundreds of raw overlaps into one error group)
  • Named nets — give every net identity so shorts can report which nets are involved

See the contributing docs for the full roadmap.

See Also

Topic Where
Open detection details Open Detection
Short detection details Short Detection
Equivalent ports Equivalent Ports
Extraction pipeline Extraction Overview
FAQ FAQ