Skip to content

Layout vs. Schematic (LVS)

Download notebook (.zip)

A layout-versus-schematic (LVS) check extracts the connectivity of a layout from its geometry and compares it with the circuit the designer intended, the schematic. It reports nets the layout leaves open, nets the layout joins that should be separate (shorts), and devices connected to the wrong ports.

A photonic chip has two kinds of nets:

  • Optical nets are carried by the waveguide core. A waveguide joins the device ports it touches, and two waveguides that cross or overlap join their nets as well, because light can pass from one to the other.
  • Electrical nets are carried by the metals and the vias, as on an electronic chip. A heater or a p-n junction is a device between two terminals, so a metal shape that bridges its two ends is a short.

lvs_check runs the comparison in KLayout from three inputs:

  • The schematic. PhotonForge writes it as a SPICE netlist from Component.get_netlist: one subcircuit per cell, whose pins are the cell’s ports and terminals. Connections, virtual connections and virtual nets become nets. A route built by the parametric routing functions is written as a wire between its two ends, so a routed component and its unrouted version give the same schematic.
  • The pin markers. The exported layout holds shapes only, no ports. The runset finds the pins of each cell from marker boxes and text labels drawn in the layout.
  • The runset. A KLayout LVS script, usually part of the PDK, that reads the layers, declares which ones conduct, extracts the nets and runs the comparison.

This guide uses SiPhoPDK, a demonstration PDK for PhotonForge: a synthetic 220 nm silicon-on-insulator process for the O-band, with silicon nitride, germanium photodiodes, a heater and two metal layers. It ships a KLayout LVS runset that extracts both the optical and the electrical nets, and every SiPhoPDK cell draws the markers the runset reads. SiPhoPDK is not a foundry PDK and must not be used for a tape-out.

The guide builds a Mach-Zehnder interferometer (MZI) with a heater in one arm and two bond pads, checks it, then finds a missing waveguide (an open) and a metal bar across the heater traces (a short), and shows where each fault is in the layout. The full API is described in Layout Verification.

  1. Install the full KLayout application, version 0.29.12 or newer, from the KLayout download page. PhotonForge searches for it in PATH and in the common install locations; set pf.config.klayout_executable when it is installed elsewhere.
  2. Install SiPhoPDK with pip install siphopdk-forge. The LVS runset is part of the package, which this guide imports as pdk.
import photonforge as pf
import siphopdk as pdk
from photonforge.live_viewer import LiveViewer
# Start the LiveViewer to see the layouts in a browser
viewer = LiveViewer()
LiveViewer started at http://127.0.0.1:45293. Call its 'stop()' method to free the port.

pdk.siphopdk() creates the SiPhoPDK technology. We add one annotation layer, LVS_ERROR, to draw the faulty nets on the layout later. It is not part of the process.

# Create the SiPhoPDK technology
tech = pdk.siphopdk()
# Add a layer to highlight faulty nets, outside the layers the runset reads
tech.add_layer("LVS_ERROR", pf.LayerSpec((99, 0), "Faulty net", "#ff4500cc", "xx"))
# Use the technology for every new component
pf.config.default_technology = tech

The MZI uses three SiPhoPDK cells:

  • y_branch, a 1x2 splitter with ports P0 (input), P1 and P2 (outputs);
  • thermal_phase_shifter, a strip waveguide with a TiN heater, with optical ports P0 and P1 and heater terminals HTRP and HTRN;
  • bond_pad, a pad on the second metal with a landing on the first metal, with terminals M1 and T.

Every SiPhoPDK cell draws the markers that the runset reads: its outline on DEVREC, a box on each optical port on PINREC, a box on each electrical terminal on PINREC_E, and the port and terminal names as text. The phase shifter shows all of them:

# Build the three cells with their default parameters
splitter = pdk.component.y_branch()
heater = pdk.component.thermal_phase_shifter()
pad = pdk.component.bond_pad()
viewer(heater)

The parametric component below builds the MZI with component_from_netlist. One Y-branch splits the light and a second Y-branch, rotated by 180°, combines it. The upper arm runs through the phase shifter, and each heater terminal is wired to its own bond pad.

The component is built in two forms:

  • routed=False joins the optical ports with virtual connections and the heater terminals with virtual nets. This is the design intent, the form a schematic editor produces.
  • routed=True draws the waveguides with route and the metal traces with route_manhattan. This is the layout.
@pf.parametric_component
def create_mzi(*, routed=True):
# Place the splitter, the combiner, the phase shifter and the two pads
netlist = {
"name": "MZI",
"instances": {
"split": splitter,
"combine": {"component": splitter, "origin": (200, 0), "rotation": 180},
"heater": {"component": heater, "origin": (50, 30)},
"pad_p": {"component": pad, "origin": (30, 150)},
"pad_n": {"component": pad, "origin": (170, 150)},
},
# Expose the optical input and output, and the top metal of both pads
"ports": [("split", "P0"), ("combine", "P0")],
"terminals": [("pad_p", "T", "HEATER_P"), ("pad_n", "T", "HEATER_N")],
}
# Optical links: the upper arm through the phase shifter, and the lower arm
optical = [
(("split", "P1"), ("heater", "P0")),
(("heater", "P1"), ("combine", "P2")),
(("split", "P2"), ("combine", "P1")),
]
# Electrical links: each heater terminal to the first metal of its pad
electrical = [
(("heater", "HTRP"), ("pad_p", "M1")),
(("heater", "HTRN"), ("pad_n", "M1")),
]
if routed:
# Draw the waveguides with a 10 µm bend radius, and the metal traces
netlist["routes"] = [(*link, {"radius": 10}) for link in optical]
netlist["terminal routes"] = electrical
else:
# Declare the same links as intent only
netlist["virtual connections"] = optical
netlist["virtual nets"] = [list(link) for link in electrical]
return pf.component_from_netlist(netlist)
# Build the design intent and the layout
intent = create_mzi(routed=False)
layout = create_mzi()
viewer(layout)
Layout
Schematic

pdk.lvs_check calls lvs_check with the SiPhoPDK runset, pdk.LVS_RULES. It also passes a pre-export function, pdk.verification.lvs_pins, that draws pin boxes and names at the ports and terminals of every circuit in the design. The SiPhoPDK cells carry their own markers, but a circuit that places them, such as the MZI, does not, and its exposed ports have to become the pins of the top cell.

Without a schematic argument, the schematic is written from the component itself. Since routes are written as wires, the schematic of the routed layout is the same as the schematic of the design intent.

# Compare the layout with the schematic written from its own netlist
results = pdk.lvs_check(layout)
print("LVS verdict:", results.verdict)
KLayout version: 0 s - KLayout version: 1 s \ KLayout LVS: 0 s - KLayout LVS: 1 s \ KLayout LVS: 1 s | KLayout LVS: 1 s / KLayout LVS: 2 s - KLayout LVS: 2 s \ KLayout LVS: 2 s | KLayout LVS: 2 s / KLayout LVS: 3 s - KLayout LVS: 3 s \ KLayout LVS: 3 s | KLayout LVS: 4 s / KLayout LVS: 4 s - KLayout LVS: 4 s \ KLayout LVS: 5 s | KLayout LVS: 5 s / Reading results: 0 s - Reading results: 1 s \ Reading results: 1 s | Reading results: 1 s / Reading results: 2 s - Reading results: 2 s \ Reading results: 2 s | Reading results: 2 s / KLayout LVS finished in 9 s
LVS verdict: match

The verdict is match, match_with_warnings or mismatch. To compare the layout with a different design intent, pass that intent as the schematic:

# Compare the layout with the design intent drawn with virtual connections and nets
results = pdk.lvs_check(layout, schematic=intent, show_progress=False)
print(results)
LVS verdict: match
Circuits: 4 compared: 4 match
Exit code: 0

Printing the results gives a summary: the circuits compared, every net, pin and device pair that did not match, and the messages from KLayout. Here every circuit matches: each SiPhoPDK cell matches its subcircuit, and the MZI joins exactly the ports and terminals that the design intent joins.

The SiPhoPDK runset, pdk.LVS_RULES, makes these choices:

  • Optical. The silicon and nitride cores outside every DEVREC outline are interconnect: they join the PINREC boxes they touch into one net. The core inside an outline belongs to its device and is removed, or the device would join all its own ports.
  • Electrical. Only the contact implants, the vias and the two metals conduct. The heater film and the silicon of a junction do not, so a heater and a p-n junction each show as two pins, and a metal trace that bridges their two ends is a short.
  • Names. The text label on each pin box gives the pin its name, which the comparison matches to the port and terminal names of the schematic.
  • Route cells. The routes exist in the layout but not in the schematic. The runset flattens them into their parent with align before the comparison.

A connection that is missing from the layout is an open. To show one, we copy the layout and remove the route of the lower arm, then compare the copy with the design intent.

# Copy the layout and remove the route of the lower arm, its eighth reference
open_arm = layout.copy()
open_arm.remove(open_arm.references[7])
viewer(open_arm)
Layout
Schematic
# Compare the copy with the design intent
results = pdk.lvs_check(open_arm, schematic=intent, show_progress=False)
print(results)
LVS verdict: mismatch
Circuits: 4 compared: 3 match, 1 nomatch (MZI)
Mismatches in MZI:
net $I1 <-> (none): mismatch
layout: ybranch $2 P1
net $I3 <-> N2: mismatch
layout: ybranch $1 P2
schematic: YBRANCH 0 P2, YBRANCH 1 P1
subcircuit $2 <-> 1: mismatch (layout: ybranch at (200, 0), rotated 180°)
Messages:
error in MZI: Net $I3 is not matching any net from reference netlist
error in MZI: Net $I1 is not matching any net from reference netlist
info in MZI: Connecting nets $I3 and $I1 is making a better match to net N2 from reference netlist (fuzziness 0 nodes)
Exit code: 0

KLayout names the layout nets ($I1 and $I3 here), and the schematic nets are named after a port of the cell, or N1, N2, … for internal nets. The summary lists the ports each net touches:

  • Schematic net N2 should join P2 of the splitter with P1 of the combiner.
  • In the layout, these two ports are on two separate nets, each touching one port only.

KLayout’s last message says it directly: connecting the two layout nets would make them match N2, which is what the missing route does.

A short joins nets that should be separate. Here we add a bar on the first metal, M1, across the two traces that lead to the bond pads. The bar connects the two ends of the heater.

# Copy the layout and draw an M1 bar across the two heater traces
shorted = layout.copy()
shorted.add("M1", pf.Rectangle((20, 100), (180, 102)))
viewer(shorted)
Layout
Schematic
# Compare the shorted copy with the design intent
results = pdk.lvs_check(shorted, schematic=intent, show_progress=False)
print(results)
LVS verdict: mismatch
Circuits: 4 compared: 3 match, 1 nomatch (MZI)
Mismatches in MZI:
net (none) <-> HEATER_N: mismatch
schematic: pin HEATER_N, TPS_L100P0 2 HTRN, PAD_S80P0 4 M1,T
net (none) <-> HEATER_P: mismatch
schematic: pin HEATER_P, TPS_L100P0 2 HTRP, PAD_S80P0 3 M1,T
net HEATER_N,HEATER_P <-> (none): mismatch
layout: pin HEATER_N,HEATER_P, pad_s80p0 $3 M1,T, tps_l100p0 $4 HTRP, tps_l100p0 $4 HTRN, pad_s80p0 $5 M1,T
subcircuit (none) <-> 3: mismatch (schematic: PAD_S80P0 at (30, 150))
subcircuit (none) <-> 4: mismatch (schematic: PAD_S80P0 at (170, 150))
subcircuit $3 <-> (none): mismatch (layout: pad_s80p0 at (170, 150))
subcircuit $5 <-> (none): mismatch (layout: pad_s80p0 at (30, 150))
subcircuit $4 <-> 2: mismatch (layout: tps_l100p0 at (50, 30))
Messages:
error in MZI: Net HEATER_N,HEATER_P is not matching any net from reference netlist
Exit code: 0

The layout has a single net named HEATER_N,HEATER_P, which carries both pad labels and both heater terminals, where the schematic has two separate nets. Because the merged net has no partner, the devices on it (the phase shifter and the two pads) cannot be paired either, and they are listed as mismatches too.

Each layout net in the results carries its geometry, per layer of the runset. Drawing the shapes of the mismatched layout nets on the LVS_ERROR layer shows the shorted net: both traces, and the bar that joins them.

# Put the layout and the faulty nets together in one component
highlight = pf.Component("MZI_LVS")
highlight.add_reference(shorted)
# Go through the net pairs of the top cell, which is the cell that did not match
for pair in results.top.nets:
# Skip matched nets and schematic nets with no partner in the layout
if pair.status == "match" or pair.layout is None:
continue
# Draw the shapes of the mismatched layout net, on every layer
for polygons in pair.layout.shapes.values():
for polygon in polygons:
highlight.add("LVS_ERROR", pf.Polygon(polygon))
viewer(highlight)
Layout
Schematic

results_file keeps KLayout’s own results database. It can be opened in KLayout’s netlist browser (Tools > Netlist Browser) to step through the nets on the layout, or read back later with load_lvs_results without running the check again.

# Run the check again and keep the KLayout results database
results = pdk.lvs_check(
shorted, schematic=intent, results_file="mzi.lvsdb", show_progress=False
)
# Read the database back without running the comparison
saved = pf.load_lvs_results("mzi.lvsdb", show_progress=False)
print("LVS verdict from the saved results:", saved.verdict)
LVS verdict from the saved results: mismatch

pdk.lvs_check(component) is a short form of

pf.lvs_check(
component,
pdk.LVS_RULES,
engine="klayout",
pre_export_function=pdk.verification.lvs_pins(component),
)

The same call runs the runset of any other PDK: give its path as the rules file, and a pre-export function that draws the markers that runset reads at the ports of the circuits. layout_pins is such a function for a generic optical runset: it draws an outline over the core of each device, a box on each port and the port names, on the layers you choose.

StepFunctionWhat it does
Design intentadd_virtual_connection, virtual nets, component_from_netlistDeclares which ports and terminals a route will join
Layoutroute, route_manhattan and the other routing functionsDraws the waveguides and traces, which the schematic writes as wires
Pin markersSiPhoPDK cells, pdk.verification.lvs_pins, layout_pinsDraw outlines, pin boxes and names in the exported layout
Comparisonlvs_check, pdk.lvs_checkWrites the schematic, runs the runset, and returns the verdict
ResultsLVSResultsMismatched nets with their ports and geometry
Saved resultsload_lvs_resultsReads a .lvsdb file without running the check

For design rules such as minimum width and spacing, see the Design Rule Check guide, which uses the SiPhoPDK rule deck.