Design Rule Check

05ce69f00a3c46a1b6d9ef7cfb820415

A design rule check (DRC) measures every shape of a layout against the limits of a fabrication process, such as minimum widths, minimum gaps and enclosures. This guide shows how to run one on a PhotonForge component with drc_check, read the violations from the returned DRCResults, and update the layout until the check is clean.

drc_check exports the component to GDSII, runs the rules file through the verification engine and reads the results database back, so the whole loop stays in Python. Two engines are supported: KLayout, used in this guide, and Calibre, which runs an SVRF rule deck. Both return the same DRCResults. See Layout Verification for the full API.

Prerequisites

  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. Provide a KLayout DRC runset. The runset reads the layout from source($gdsfile) and writes its report with report("...", $resultsfile); drc_check sets both variables. This guide uses a slightly modified SiEPIC EBeam runset, SiEPIC_EBeam.drc.

[1]:
import photonforge as pf
import siepic_forge as siepic

Set up the technology

PhotonForge uses a technology (PDK) definition to map layer names, port specs, and design rules.

In the next cell, we select the SiEPIC EBeam technology so that:

  • Named port specs like "TE_1550_500" resolve correctly.

  • Geometry is written on the expected technology layers (so the DRC runset can interpret it).

[2]:
# Set default technology to the EBeam technology
pf.config.default_technology = siepic.ebeam()

Create a parametric layout (PBS)

Next, we build a simple SWG-assisted PBS layout.

For this tutorial, we intentionally start with a layout that triggers a few DRC violations so we can see how they show up in the results, and then address them with a couple of small layout updates (including adding a floorplan).

[3]:
# Define a parametric component for the SWG-assisted PBS
@pf.parametric_component
def create_pbs(
    *,
    spec_strip="TE_1550_500",
    spec_swg="TE_1550_500",
    w_corrugation=0.130,
    gap=0.070,
    swg_period=0.240,
    swg_duty_cycle=0.2,
    n_periods=10,
    s_bend_length=6,
    s_bend_offset=2,
):
    # Resolve string-based specs from the default technology if needed
    if isinstance(spec_strip, str):
        spec_strip = pf.config.default_technology.ports[spec_strip]
    if isinstance(spec_swg, str):
        spec_swg = pf.config.default_technology.ports[spec_swg]

    # Extract effective silicon widths from the provided PortSpecs
    w_strip, _ = spec_strip.path_profile_for("Si")
    w_swg, _ = spec_swg.path_profile_for("Si")

    # Compute total coupling length from SWG period and number of periods
    coupling_length = n_periods * swg_period

    # Create one SWG unit cell component consisting of ridge + groove
    unit_cell = pf.Component("SWG Period")

    # Build the ridge and groove rectangles for one period
    ridge_length = swg_duty_cycle * swg_period
    ridge = pf.Rectangle((0, -w_swg / 2), (ridge_length, w_swg / 2))
    groove = pf.Rectangle(
        (ridge_length, -w_swg / 2), (swg_period, w_swg / 2 - w_corrugation)
    )

    # Add ridge and groove into the unit cell along +x
    unit_cell.add((1, 0), ridge, groove)

    # Create the parent PBS component
    pbs = pf.Component("PBS")

    # Instantiate the SWG array by repeating the unit cell N times
    swg_array = pf.Reference(
        component=unit_cell, columns=n_periods, rows=1, spacing=(swg_period, 0.0)
    )

    # Add a short straight continuation for the SWG waveguide after coupling region
    swg_wg = pf.Rectangle(
        (coupling_length, -w_swg / 2),
        (coupling_length + s_bend_length + 0.1, w_swg / 2),
    )

    # Route the strip waveguide: lead-in, straight through coupler, then S-bend
    strip_wg = (
        pf.Path(origin=(-1.0, gap + (w_swg + w_strip) / 2), width=w_strip)
        .segment(endpoint=(coupling_length + 1.0, 0), relative=True)
        .s_bend(endpoint=(s_bend_length, s_bend_offset), relative=True)
    )

    # Add geometry to the PBS component (placed along +x)
    pbs.add((1, 0), swg_array, strip_wg, swg_wg)

    return pbs


# Instantiate and view the PBS component with default parameters
pbs = create_pbs()

Running DRC

drc_check takes the component and the rules file. It writes the layout to a temporary GDSII file, runs KLayout in batch mode and loads the markers it reports. The rules and operations live in the separate runset file, written in KLayout’s runset syntax.

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

[4]:
# Export the component, run the runset through KLayout and load the markers
results = pf.drc_check(pbs, "SiEPIC_EBeam.drc", results_file="drc_results.lyrdb")
KLayout DRC finished in 5 s

Inspecting results

The output of drc_check is a DRCResults object holding the markers of every rule in the runset.

results.is_clean tells whether the layout passed:

[5]:
if results.is_clean:
    print("DRC passed!\n")
else:
    print("DRC did not pass!\n")
DRC did not pass!

You can print a high-level summary of any violations.

  • Total violations is the total number of marker shapes across all rules.

  • Violations by category groups markers by the rule name (for example, Boundary, Si_width, etc.).

In many flows, Boundary indicates that the layout is missing a recognized chip boundary / floorplan region that the runset uses to define the allowed design area:

[6]:
print(results)
DRC results summary
--------------------------------
Total violations: 14

Violations by category:
Devices: 0
Boundary: 4
Si_width: 10
Si_space: 0
SiN_width: 0
SiN_space: 0
M1_width: 0
M1_space: 0
M2_width: 0
M2_space: 0
MLOpen_width: 0
MLOpen_space: 0
M2_M1_overlap: 0
SiEPIC-1a: 0
DT_Metal_separation: 0

What these violations mean

  • Si_width violations indicate that at least one silicon feature in the layout is narrower than the minimum allowed by the runset. In this example, the SWG unit cell creates very short ridge segments, which can trigger minimum-width checks.

  • Boundary violations indicate that the runset can’t find (or can’t use) a proper chip boundary / floorplan region for the design. Many foundry runsets use this region to define where geometry is allowed.

Below we’ll first show how to access the categories and markers programmatically, then we’ll fix the layout by (1) adjusting the component parameters and (2) adding a FloorPlan region.

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

[7]:
print(pf.load_drc_results("drc_results.lyrdb"))
KLayout DRC results finished in 3 s
DRC results summary
--------------------------------
Total violations: 14

Violations by category:
Devices: 0
Boundary: 4
Si_width: 10
Si_space: 0
SiN_width: 0
SiN_space: 0
M1_width: 0
M1_space: 0
M2_width: 0
M2_space: 0
MLOpen_width: 0
MLOpen_space: 0
M2_M1_overlap: 0
SiEPIC-1a: 0
DT_Metal_separation: 0

The names of all the checked rules (categories) are in results.categories:

[8]:
results.categories
[8]:
('Devices',
 'Boundary',
 'Si_width',
 'Si_space',
 'SiN_width',
 'SiN_space',
 'M1_width',
 'M1_space',
 'M2_width',
 'M2_space',
 'MLOpen_width',
 'MLOpen_space',
 'M2_M1_overlap',
 'SiEPIC-1a',
 'DT_Metal_separation')

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

[9]:
# Description of the 'Si_width' rule, as written in the runset
print(results["Si_width"].description)

# Number of violation shapes found for the 'Boundary' rule
print(results["Boundary"].count)

# All violation marker shapes for the 'Si_width' rule
print(results["Si_width"].markers)
Si minimum feature size violation; min 60 nm
4
(EdgePairMarker(cell='PBS', edge_pair=(((0.0, 0.07), (0.0, 0.25)), ((0.048, 0.25), (0.048, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((0.24, 0.12), (0.24, 0.25)), ((0.288, 0.25), (0.288, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((0.48, 0.12), (0.48, 0.25)), ((0.528, 0.25), (0.528, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((0.72, 0.12), (0.72, 0.25)), ((0.768, 0.25), (0.768, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((0.96, 0.12), (0.96, 0.25)), ((1.008, 0.25), (1.008, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((1.2, 0.12), (1.2, 0.25)), ((1.248, 0.25), (1.248, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((1.44, 0.12), (1.44, 0.25)), ((1.488, 0.25), (1.488, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((1.68, 0.12), (1.68, 0.25)), ((1.728, 0.25), (1.728, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((1.92, 0.12), (1.92, 0.25)), ((1.968, 0.25), (1.968, 0.12))), symmetric=True), EdgePairMarker(cell='PBS', edge_pair=(((2.16, 0.12), (2.16, 0.25)), ((2.208, 0.25), (2.208, 0.12))), symmetric=True))

Understanding markers (where the violation occurs)

Each category contains one or more markers that point to the location of the failing geometry in the layout. Each marker also names the cell it was found in, in that cell’s coordinates; results.in_top_cell(marker) moves it into the top cell. These are useful for debugging:

  • Use the counts (e.g. results["Si_width"].count) to see how widespread a problem is.

  • Use the markers (e.g. results["Si_width"].markers) to see exactly where the rule failed.

In the next section, we add text labels near the marker locations to make it easier to visually inspect the problematic regions in a layout viewer.

Adding Labels on Violations

It is possible to add labels on the location of violations.

[10]:
# Get the list of violation markers for the 'Si_width' rule
width_markers = results["Si_width"].markers

for i, marker in enumerate(width_markers):

    x_center = marker.edge_pair[0][0][0]
    y_center = (marker.edge_pair[0][0][1] + marker.edge_pair[0][1][1]) / 2

    # Create the label
    label_text = f"Si_width_viol_{i+1}"
    violation_label = pf.Label(
        text=label_text,
        origin=(x_center, y_center),
        anchor='NW', # Anchor the label text to the northwest of the violation
        scaling=0.5 # Use a smaller scale for better visualization
    )

    # Add the label to the component on the 'Text' layer
    pbs.add("Text", violation_label)

pbs.write_gds("pbs_with_labels.gds")
[10]:
Layout
Schematic

Fixing the violations

From the summary above we have two relevant categories:

  • Si_width: some silicon features are below the minimum width required by the runset (in this layout, the SWG “ridge” length per period is very short, creating narrow features).

  • Boundary: the runset expects the design to be contained within a recognized chip boundary / floorplan region (commonly provided on a FloorPlan/chip area layer).

A typical workflow is:

  1. Fix geometry rule violations (like Si_width) by updating the layout parameters and regenerating the GDS.

  2. Fix boundary rule violations by adding a FloorPlan that encloses the design.

In the next cell we regenerate the PBS with a less aggressive SWG duty cycle to address the Si_width violations.

[11]:
# Update the component parameters to address Si_width violations
# (increase SWG duty cycle -> longer ridges -> wider minimum feature)
pbs.update(swg_duty_cycle=0.3)

results_fixed = pf.drc_check(pbs, "SiEPIC_EBeam.drc")
print(f"Si_width violations after update: {results_fixed['Si_width'].count}")
print(f"Boundary violations before adding FloorPlan: {results_fixed['Boundary'].count}")
KLayout DRC finished in 5 s
Si_width violations after update: 0
Boundary violations before adding FloorPlan: 4

From here on, pbs refers to the updated component (after the Si_width fix). Now we add DevRec to address the Boundary violations.

[12]:
# Define the margin and calculate the box envelope around the whole geometry
margin = 1.0  # For example, use a 1 µm margin on all sides
chip_area_rect = pf.envelope(pbs, offset=margin, use_box=True)

# Add the rectangle to the component on the "Chip design area" layer
pbs.add("Floor Plan-Not Fabricated", chip_area_rect)
[12]:
Layout
Schematic
[13]:
results = pf.drc_check(pbs, "SiEPIC_EBeam.drc")
print(f"The number of Boundary violations is now: {results['Boundary'].count}")
KLayout DRC finished in 5 s
The number of Boundary violations is now: 0

Final DRC check

After adding the floorplan, re-running DRC should eliminate the Boundary category. Since we also updated the layout geometry to address the width rule earlier, the final result should be clean (no violations).

[14]:
print(results)
print(f"DRC clean: {results.is_clean}")
DRC results summary
--------------------------------
Total violations: 0

Violations by category:
Devices: 0
Boundary: 0
Si_width: 0
Si_space: 0
SiN_width: 0
SiN_space: 0
M1_width: 0
M1_space: 0
M2_width: 0
M2_space: 0
MLOpen_width: 0
MLOpen_space: 0
M2_M1_overlap: 0
SiEPIC-1a: 0
DT_Metal_separation: 0

DRC clean: True

Expected result: after the geometry update and adding FloorPlan, print(results) should report 0 total violations, and DRC clean should be True.