Skip to content

Design Rule Check

Download notebook (.zip)

A design rule check (DRC) measures every shape of a layout against the limits of a fabrication process, such as minimum widths, minimum gaps, enclosures and the area a design may occupy. The rules are written in a rule deck that the foundry ships with its process design kit (PDK).

drc_check runs a rule deck on a PhotonForge component. It exports the component to GDSII, runs the deck in KLayout and reads the results database back into a DRCResults object, so the whole loop stays in Python. Calibre, which runs an SVRF rule deck, is also supported and returns the same results. See Layout Verification for the full API.

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 DRC deck of 43 rules for width, space, enclosure, placement and area, and the deck states where each value comes from. SiPhoPDK is not a foundry PDK and must not be used for a tape-out.

The guide checks a directional coupler whose gap is below the minimum silicon spacing, reads the violations, draws them on the layout, and fixes the layout until the check is clean.

  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 rule deck 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:35497. Call its 'stop()' method to free the port.

pdk.siphopdk() creates the SiPhoPDK technology: its layers, layer stack and port specifications. Setting it as the default makes every new component use it.

We add one annotation layer, DRC_MARKER, to draw the violations on the layout later. It is not part of the process.

# Create the SiPhoPDK technology
tech = pdk.siphopdk()
# Add a layer to draw the DRC markers on, outside the layers the deck checks
tech.add_layer("DRC_MARKER", pf.LayerSpec((99, 0), "DRC marker", "#ff4500cc", "xx"))
# Use the technology for every new component
pf.config.default_technology = tech

The layer specifications of SiPhoPDK also carry the basic width and spacing rules of each layer. For the silicon core layer SI, both are 150 nm:

# Minimum width and spacing stored on the silicon layer (μm)
print("SI minimum width:", tech.layers["SI"].min_width)
print("SI minimum spacing:", tech.layers["SI"].min_spacing)
SI minimum width: 0.15
SI minimum spacing: 0.15

The SiPhoPDK directional coupler, pdk.component.directional_coupler, takes coupling_distance, the distance between the centers of its two waveguides in the coupling section. The gap between the silicon edges is that distance minus the core width, 400 nm for the strip waveguide SI_STRIP_O.

We ask for a gap of 120 nm, below the 150 nm minimum spacing, so the check has something to find.

# Read the core width of the strip waveguide from its port specification
core_width, _ = tech.ports["SI_STRIP_O"].path_profile_for("SI")
# Gap between the silicon edges in the coupling section (μm)
gap = 0.12
# Build the coupler with a center-to-center distance of core width plus gap
coupler = pdk.component.directional_coupler(coupling_distance=core_width + gap)
viewer(coupler)

drc_check takes the component and the rule deck. SiPhoPDK gives the path of its deck as pdk.DRC_RULES. The deck reads the layout with source($gdsfile) and writes its report with report(..., $resultsfile); drc_check sets both variables.

The optional results_file argument keeps KLayout’s own results database, here drc_results.lyrdb. It can be opened in KLayout’s marker browser or loaded again later.

# Export the coupler, run the SiPhoPDK deck in KLayout and load the markers
results = pf.drc_check(coupler, pdk.DRC_RULES, results_file="drc_results.lyrdb")
KLayout version: 0 s - KLayout DRC: 0 s - KLayout DRC: 1 s \ KLayout DRC: 1 s | KLayout DRC: 1 s / KLayout DRC: 2 s - KLayout DRC: 2 s \ KLayout DRC: 2 s | KLayout DRC: 2 s / KLayout DRC: 3 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 DRC finished in 6 s

SiPhoPDK also provides pdk.drc_check(component), which is the same call with the deck filled in. The rest of this guide uses it.

The output of drc_check is a DRCResults object holding the markers of every rule in the deck. results.is_clean is the verdict:

# Check whether any rule reported a violation
print("DRC clean:", results.is_clean)
DRC clean: False

print(results) lists every rule of the deck with its count, which is long for a deck of 43 rules. results.violation_counts holds the same numbers as a dictionary, from which we keep the rules that failed:

# Keep the rules with at least one violation
failed = {rule: count for rule, count in results.violation_counts.items() if count > 0}
print(failed)
{'FP.1': 2, 'SI.S.1': 13}

Indexing the results with a rule name returns that rule’s violations, with the description written in the deck, the number of markers and the markers themselves:

# Print the description of each failed rule, as written in the deck
for rule in failed:
print(f"{rule}: {results[rule].description}")
FP.1: Geometry outside FLOORPLAN
SI.S.1: SI space < 150 nm
  • SI.S.1 reports silicon shapes closer than 150 nm. The 120 nm gap of the coupling section is the main marker; the deck also marks the start of each S-bend, where the arms are still closer than the rule.
  • FP.1 reports geometry outside the FLOORPLAN layer. The SiPhoPDK deck requires every drawn shape to lie inside the floor plan of the die, and the coupler has none yet.

KLayout checks the layout hierarchically, so each marker is reported once, in the cell where the violation occurs and in the coordinates of that cell. results.violated_cells lists the cells with at least one marker. results.in_top_cell(marker) moves a marker into the coordinates of the top cell. Here the coupler is the top cell, so both are the same.

# Cells with at least one violation
print(results.violated_cells)
# First 'SI.S.1' marker, a polygon in the coordinates of its cell
print(results["SI.S.1"].markers[0])
('dc_lc15p0_g0p52',)
PolygonMarker(cell='dc_lc15p0_g0p52', hull=((14.493, 4.191), (14.558, 4.326), (14.631, 4.325), (14.558, 4.194)), holes=())

Each SI.S.1 marker is a polygon. Adding the polygons on the DRC_MARKER layer, in a component that also holds the coupler, shows where the rule failed:

# Put the coupler and the markers together in one component
marked = pf.Component("COUPLER_DRC")
marked.add_reference(coupler)
# Draw every 'SI.S.1' marker, moved into the coordinates of the top cell
for marker in results["SI.S.1"].markers:
marker = results.in_top_cell(marker)
marked.add("DRC_MARKER", pf.Polygon(marker.hull))
viewer(marked)
Layout
Schematic

A results database written earlier can be read back with load_drc_results, without running the check again:

# Read the saved results database
saved = pf.load_drc_results("drc_results.lyrdb", show_progress=False)
print({rule: count for rule, count in saved.violation_counts.items() if count > 0})
{'FP.1': 2, 'SI.S.1': 13}

We fix the two rules in turn:

  1. SI.S.1: widen the gap to 200 nm. The coupler is a parametric component, so update rebuilds it with the new distance.
  2. FP.1: place the coupler on a die that has a floor plan around it.
# Widen the gap to 200 nm, which rebuilds the coupler
coupler.update(coupling_distance=core_width + 0.2)
# Check the coupler again with the SiPhoPDK deck
results = pdk.drc_check(coupler, show_progress=False)
print({rule: count for rule, count in results.violation_counts.items() if count > 0})
{'FP.1': 2}

Only FP.1 is left. A die is a cell that holds the design and the FLOORPLAN rectangle that encloses it. envelope gives a box around the coupler, enlarged by a margin:

# Margin between the coupler and the edge of the floor plan (μm)
margin = 10.0
# Place the coupler on a die with a floor plan around it
die = pf.Component("COUPLER_DIE")
die.add_reference(coupler)
die.add("FLOORPLAN", pf.envelope(coupler, offset=margin, use_box=True))
viewer(die)
Layout
Schematic
# Check the die with the SiPhoPDK deck
results = pdk.drc_check(die, show_progress=False)
print("DRC clean:", results.is_clean)
DRC clean: True

The die passes every rule of the deck.

  • Another rule deck. pf.drc_check(component, "deck.drc") runs any KLayout deck that reads $gdsfile and writes $resultsfile. A deck that uses other variable names is driven with variable_names, for example variable_names={"gdsfile": "input", "resultsfile": "report"}. With engine="calibre", the deck is an SVRF rule deck run by Calibre.
  • Waivers. Geometry inside the SiPhoPDK layer EXCL_DRC (95, 0) is waived by the deck. Other PDKs have their own waiver layers.
  • The LiveViewer. The LiveViewer has a DRC section that runs a rule deck on the component on screen, with KLayout or Calibre, and draws the markers on the layout. It also opens an existing report, such as drc_results.lyrdb above.
  • Rules in the technology. The min_width and min_spacing of a LayerSpec are used as defaults by route_electrical, so that metal routes are drawn within the rules in the first place.

For connectivity checks between the layout and the schematic, see the Layout vs. Schematic guide.