Layout vs. Schematic (LVS)

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.
Prerequisites
Section titled “Prerequisites”- Install the full KLayout application, version 0.29.12 or newer, from the KLayout download page. PhotonForge searches for it in
PATHand in the common install locations; setpf.config.klayout_executablewhen it is installed elsewhere. - Install SiPhoPDK with
pip install siphopdk-forge. The LVS runset is part of the package, which this guide imports aspdk.
import photonforge as pfimport siphopdk as pdkfrom photonforge.live_viewer import LiveViewer
# Start the LiveViewer to see the layouts in a browserviewer = LiveViewer()LiveViewer started at http://127.0.0.1:45293. Call its 'stop()' method to free the port.Setting up the technology
Section titled “Setting up the technology”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 technologytech = pdk.siphopdk()
# Add a layer to highlight faulty nets, outside the layers the runset readstech.add_layer("LVS_ERROR", pf.LayerSpec((99, 0), "Faulty net", "#ff4500cc", "xx"))
# Use the technology for every new componentpf.config.default_technology = techThe SiPhoPDK cells
Section titled “The SiPhoPDK cells”The MZI uses three SiPhoPDK cells:
y_branch, a 1x2 splitter with portsP0(input),P1andP2(outputs);thermal_phase_shifter, a strip waveguide with a TiN heater, with optical portsP0andP1and heater terminalsHTRPandHTRN;bond_pad, a pad on the second metal with a landing on the first metal, with terminalsM1andT.
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 parameterssplitter = pdk.component.y_branch()heater = pdk.component.thermal_phase_shifter()pad = pdk.component.bond_pad()
viewer(heater)Building the MZI
Section titled “Building the MZI”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=Falsejoins 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=Truedraws the waveguides with route and the metal traces with route_manhattan. This is the layout.
@pf.parametric_componentdef 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 layoutintent = create_mzi(routed=False)layout = create_mzi()viewer(layout)Running the check
Section titled “Running the check”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 netlistresults = 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 sLVS verdict: matchThe 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 netsresults = pdk.lvs_check(layout, schematic=intent, show_progress=False)print(results)LVS verdict: matchCircuits: 4 compared: 4 match
Exit code: 0Printing 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.
How the runset extracts the nets
Section titled “How the runset extracts the nets”The SiPhoPDK runset, pdk.LVS_RULES, makes these choices:
- Optical. The silicon and nitride cores outside every
DEVRECoutline are interconnect: they join thePINRECboxes 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
alignbefore the comparison.
Finding an open connection
Section titled “Finding an open connection”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 referenceopen_arm = layout.copy()open_arm.remove(open_arm.references[7])viewer(open_arm)# Compare the copy with the design intentresults = pdk.lvs_check(open_arm, schematic=intent, show_progress=False)print(results)LVS verdict: mismatchCircuits: 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: 0KLayout 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
N2should joinP2of the splitter withP1of 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.
Finding a short
Section titled “Finding a short”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 tracesshorted = layout.copy()shorted.add("M1", pf.Rectangle((20, 100), (180, 102)))viewer(shorted)# Compare the shorted copy with the design intentresults = pdk.lvs_check(shorted, schematic=intent, show_progress=False)print(results)LVS verdict: mismatchCircuits: 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: 0The 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.
Locating the fault
Section titled “Locating the fault”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 componenthighlight = pf.Component("MZI_LVS")highlight.add_reference(shorted)
# Go through the net pairs of the top cell, which is the cell that did not matchfor 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)Saving the results
Section titled “Saving the results”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 databaseresults = pdk.lvs_check( shorted, schematic=intent, results_file="mzi.lvsdb", show_progress=False)
# Read the database back without running the comparisonsaved = pf.load_lvs_results("mzi.lvsdb", show_progress=False)print("LVS verdict from the saved results:", saved.verdict)LVS verdict from the saved results: mismatchUsing another runset
Section titled “Using another runset”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.
Summary
Section titled “Summary”| Step | Function | What it does |
|---|---|---|
| Design intent | add_virtual_connection, virtual nets, component_from_netlist | Declares which ports and terminals a route will join |
| Layout | route, route_manhattan and the other routing functions | Draws the waveguides and traces, which the schematic writes as wires |
| Pin markers | SiPhoPDK cells, pdk.verification.lvs_pins, layout_pins | Draw outlines, pin boxes and names in the exported layout |
| Comparison | lvs_check, pdk.lvs_check | Writes the schematic, runs the runset, and returns the verdict |
| Results | LVSResults | Mismatched nets with their ports and geometry |
| Saved results | load_lvs_results | Reads 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.