KLayout is the de facto standard open-source layout viewer and editor for photonic integrated circuit design. Nearly every PIC design team uses it at some point in their flow, whether for layout editing, GDS-II inspection, DRC execution, or post-processing. What fewer teams fully use is the KLayout Python API — a complete scripting interface that exposes every element of the GDS-II file as manipulable Python objects.
Building automated photonic design checks on top of this API is one of the most practical ways to implement custom verification rules without investing in a full EDA platform. This article covers the API's structure, how it maps to photonic verification use cases, and several patterns that appear repeatedly in production check scripts.
The KLayout Python API: Basic Structure
KLayout's Python scripting environment exposes its data model through the pya module (or equivalently klayout.db for standalone use outside the GUI). The core hierarchy maps directly to GDS-II structure:
import pya
# Load a GDS file
layout = pya.Layout()
layout.read("design.gds")
# Top cell
top_cell = layout.top_cell()
# Iterate all instances in the top cell
for inst in top_cell.each_inst():
cell = layout.cell(inst.cell_index)
print(f"Instance: {cell.name}, transform: {inst.trans}")
The key objects in the API are:
pya.Layout— the full GDS-II database. Contains all cells, layers, and properties.pya.Cell— a GDS cell. Can contain shapes (polygons, paths, text) and child instances.pya.Instance— a placed cell reference, with a transformation (rotation, translation, scaling).pya.Polygon,pya.Path,pya.Text— the geometry primitives. Polygons and paths are the waveguide geometry. Text labels on specific layers encode port names and attributes.pya.LayerInfo— binds GDS layer/datatype integer pairs to semantic names via the layer map.
Finding Waveguide Geometry by Layer
The first step in any photonic check script is identifying which GDS layers correspond to which physical elements. This requires the PDK layer map — a mapping from GDS layer/datatype pairs to names like WG_CORE, WG_RIB, WG_PARTIAL. With the layer map, you can query the layout for all shapes on the waveguide core layer:
# Get the layer index for the waveguide core layer
wg_layer = layout.layer(pya.LayerInfo(1, 0)) # example: layer 1, datatype 0
# Collect all waveguide polygons in the top cell, recursively
region = pya.Region(top_cell.begin_shapes_rec(wg_layer))
# Each polygon in region is a waveguide shape
for poly in region.each():
bbox = poly.bbox()
print(f"Waveguide polygon: bbox {bbox}, area {poly.area()}")
The pya.Region object is particularly useful for check scripts because it supports boolean operations: union, intersection, and difference between shape sets. This enables checks like "find all areas where two waveguide polygons are within 500 nm of each other" — the cross-talk proximity check — by computing the overlap of dilated waveguide regions.
Proximity Check: Detecting Unintended Evanescent Coupling
One of the most valuable automated checks for PIC layouts is detecting waveguide segments that run too close to each other without being intended couplers. Two waveguides running in parallel at less than, say, 2 μm gap for more than 20 μm can act as unintended directional couplers at 1550 nm in many SOI processes. Here's a pattern for implementing this check:
PROXIMITY_GAP = 2000 # 2 µm in database units (nm)
MIN_OVERLAP_LENGTH = 20000 # 20 µm in database units
# Get all waveguide shapes
wg_region = pya.Region(top_cell.begin_shapes_rec(wg_layer))
# Grow each waveguide by half the proximity gap
grown = wg_region.sized(PROXIMITY_GAP // 2)
# Find areas where grown shapes overlap (= original shapes were within proximity_gap)
proximity_zones = grown.and_(grown)
# Subtract the original waveguides themselves (self-overlap)
proximity_zones = proximity_zones - wg_region.sized(1)
# Flag any proximity zone longer than threshold
violations = []
for zone in proximity_zones.each():
bbox = zone.bbox()
if max(bbox.width(), bbox.height()) >= MIN_OVERLAP_LENGTH:
violations.append(bbox)
This is a simplified version — production scripts need to handle cell transformations, filter out intentional coupler structures (which are in PDK cells that should be excluded from this check), and generate report entries with cell paths and coordinates. But the core logic is exactly this: dilate waveguide shapes, find overlaps, filter by overlap length.
Cell Port Width Compliance
Checking port width matching at cell boundaries requires reading port metadata from text labels. KLayout uses text shapes on specific annotation layers to encode port positions and attributes. A common convention in silicon photonics PDKs is to place a text label on a dedicated port layer at the port coordinate, with the text encoding the port name and width:
PORT_LAYER = layout.layer(pya.LayerInfo(68, 0)) # example port annotation layer
ports = {}
for shape in top_cell.begin_shapes_rec(PORT_LAYER):
if shape.shape().is_text():
text = shape.shape().text
# Parse "portname:width_nm" convention
parts = text.string.split(":")
if len(parts) == 2:
name, width_str = parts
coord = pya.Point(int(shape.shape().text.x), int(shape.shape().text.y))
ports[coord] = {"name": name, "width": int(width_str)}
# Find connected port pairs (same coordinate)
# Then check width matching
from collections import defaultdict
coord_to_ports = defaultdict(list)
for coord, port in ports.items():
coord_to_ports[coord].append(port)
for coord, port_list in coord_to_ports.items():
if len(port_list) == 2:
w1 = port_list[0]["width"]
w2 = port_list[1]["width"]
if abs(w1 - w2) > 5: # tolerance: 5 nm
print(f"Width mismatch at {coord}: {w1} nm vs {w2} nm")
The actual port label format varies per PDK; this pattern must be adapted to the specific convention. Some PDKs use GDS properties (element properties) rather than text labels for port metadata, which requires a different API call (shape.property(key)). The principle is the same.
Running Checks as DRC Scripts
KLayout supports a Ruby-based DRC scripting engine natively, accessible through the DRC menu. For Python-based checks, the preferred approach for standalone execution is to run scripts using the KLayout command-line interface:
klayout -b -r check_pic.py -rd input=design.gds -rd pdk=pdk_config.json
The -b flag runs in batch mode (no GUI). The -r flag specifies the Python script. -rd passes named parameters to the script as variables in the $input and $pdk run-time variables.
For CI/CD integration, this command-line invocation is the key. A GitHub Actions job or a tape-out pipeline stage can call KLayout in batch mode with the check script and the submitted GDS file, capture the output, and fail the pipeline if any high-severity violations are found.
Limitations of the Python API Approach
We're not saying the KLayout Python API is a complete photonic verification platform. Several important limitations apply:
Coordinate system confusion: KLayout uses internal database units (typically 1 nm or 0.001 μm per unit, depending on the GDS precision). All polygon coordinates are integers in these units. Failing to apply the correct unit conversion produces silently wrong distance measurements — a very common mistake in first-attempt check scripts.
Cell transformation handling: When iterating shapes recursively (begin_shapes_rec), KLayout applies cell transformations automatically, giving coordinates in the top cell's coordinate system. When working with instanced cells manually, you must apply the transformation yourself using inst.trans. Mixing these approaches produces inconsistent coordinates.
Performance at scale: Python-level loops over large numbers of polygons in complex PIC layouts can be slow. For production scripts on large chips, using pya.Region bulk operations (which are implemented in C++) is significantly faster than polygon-by-polygon Python loops.
No built-in photonic model: The KLayout API exposes geometry but knows nothing about photonic physics. Checking whether a coupling gap is within PDK specification requires external knowledge of what the PDK specifies — which means your check script must load and interpret a PDK configuration file alongside the GDS. Building and maintaining that PDK configuration layer is its own engineering effort.
From Scripts to a Verification Framework
A collection of individual KLayout Python check scripts is a useful starting point, but it's not a verification framework. A framework adds: a PDK configuration layer (so the same scripts work across different PDK versions without code changes), a structured output format (JSON report with violation type, location, severity, and suggested fix), a configuration interface for toggling checks on and off per design requirement, and an integration layer for invoking the full check suite from a CI pipeline or tape-out management system.
Building all of that on top of the KLayout Python API is achievable, and for teams with sufficient EDA tooling bandwidth, it's a reasonable approach. For teams where tape-out tooling is not the core competency, integrating with a dedicated photonic verification tool that already implements this framework — and can be configured to a specific PDK without writing check scripts from scratch — is often the more time-efficient path. The KLayout Python API is the right foundation for understanding what photonic verification checks actually do at the geometry level; whether you implement them yourself or use them as the underlying execution layer for a higher-level tool depends on your team's constraints.