KLayout DRC Plugin

4b6b9967057a47668ab9159d9d7a6590

This guide demonstrates how to run design rule checks (DRC) on a GDS file in PhotonForge using the KLayout-based DRC utilities.

What you’ll do

Prerequisites

  1. Have the full KLayout application installed and available on your system PATH.

  2. Provide a KLayout DRC runset script. This notebook uses a SiEPIC DRC file (with slight modification).

Please see the README for more information.

[1]:
import tidy3d as td
from tidy3d.plugins.klayout.drc import DRCResults, DRCRunner
import photonforge as pf
import siepic_forge as siepic

td.config.logging_level = "INFO"

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 and export it to GDS.

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()
pbs.write_gds("pbs.gds")
[3]:
../_images/guides_DRC_5_0.svg

Running DRC

To run DRC, create a DRCRunner and call DRCRunner.run().

The rules and operations are defined in a separate DRC runset file that follows the KLayout runset syntax. In this example, we use the SiEPIC_EBeam.drc runset.

[4]:
# Run DRC
runner = DRCRunner(drc_runset="SiEPIC_EBeam.drc", verbose=True)
results = runner.run(source="./pbs.gds")
10:02:27 EST Running KLayout DRC on GDS file 'pbs.gds' with runset
             'SiEPIC_EBeam.drc' and saving results to 'drc_results.lyrdb'...
10:02:30 EST KLayout DRC completed successfully.

Inspecting results

The output of runner.run(...) is a DRCResults object containing the evaluated rules and any violation markers.

You can quickly check pass/fail using DRCResults.is_clean:

[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.

Results can also be loaded from a KLayout DRC database file with DRCResults.load(resultsfile)

[7]:
print(DRCResults.load("drc_results.lyrdb"))
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 categories can be retrieved with DRCResults.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')

To get the markers for a specific category, index DRCResults by key:

[9]:
# This will show how many violation shapes were found for the 'Boundary' rule.
print(results["Boundary"].count)

# This will show all of the violation marker shapes for the 'Si_width' rule
print(results["Si_width"].markers)
4
(EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((0.0, 0.07), (0.0, 0.25)), ((0.048, 0.25), (0.048, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((0.24, 0.12), (0.24, 0.25)), ((0.288, 0.25), (0.288, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((0.48, 0.12), (0.48, 0.25)), ((0.528, 0.25), (0.528, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((0.72, 0.12), (0.72, 0.25)), ((0.768, 0.25), (0.768, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((0.96, 0.12), (0.96, 0.25)), ((1.008, 0.25), (1.008, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((1.2, 0.12), (1.2, 0.25)), ((1.248, 0.25), (1.248, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((1.44, 0.12), (1.44, 0.25)), ((1.488, 0.25), (1.488, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((1.68, 0.12), (1.68, 0.25)), ((1.728, 0.25), (1.728, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((1.92, 0.12), (1.92, 0.25)), ((1.968, 0.25), (1.968, 0.12)))), EdgePairMarker(attrs={}, cell='PBS', type='EdgePairMarker', edge_pair=(((2.16, 0.12), (2.16, 0.25)), ((2.208, 0.25), (2.208, 0.12)))))

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. 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]:
../_images/guides_DRC_22_0.svg

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)
pbs.write_gds("pbs_fixed.gds")

results_fixed = runner.run(source="./pbs_fixed.gds")
print(f"Si_width violations after update: {results_fixed['Si_width'].count}")
print(f"Boundary violations before adding FloorPlan: {results_fixed['Boundary'].count}")
             Running KLayout DRC on GDS file 'pbs_fixed.gds' with runset
             'SiEPIC_EBeam.drc' and saving results to 'drc_results.lyrdb'...
10:02:31 EST KLayout DRC completed successfully.
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 FloorPlan to address the Boundary violations.

[12]:
# Get the current component bounds
(min_x, min_y), (max_x, max_y) = pbs.bounds()

# Define the margin and calculate the new boundary coordinates
margin = 1.0 # For example, use a 1 µm margin on all sides

new_min_x = min_x - margin
new_min_y = min_y - margin
new_max_x = max_x + margin
new_max_y = max_y + margin

# Create the Rectangle geometry
chip_area_rect = pf.Rectangle(
    corner1=(new_min_x, new_min_y), # Bottom-left corner
    corner2=(new_max_x, new_max_y)  # Top-right corner
)

# Add the rectangle to the component on the "Chip design area" layer
pbs.add("FloorPlan", chip_area_rect)

# Write the updated GDS file to apply the change
pbs.write_gds("pbs_with_floorplan.gds")
[12]:
../_images/guides_DRC_26_0.svg
[13]:
results = runner.run(source="./pbs_with_floorplan.gds")
print(f"The number of Boundary violations is now: {results['Boundary'].count}")
             Running KLayout DRC on GDS file 'pbs_with_floorplan.gds' with
             runset 'SiEPIC_EBeam.drc' and saving results to
             'drc_results.lyrdb'...
10:02:33 EST KLayout DRC completed successfully.
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.