Skip to content

Download notebook (.ipynb)

Layers

In GDS-based photonic and electronic design, a layer is an integer pair (layer_number, datatype) that identifies a fabrication process step — for example waveguide core, metal trace, or doping implant. kfactory provides three abstractions for working with layers:

Class Best for
LayerInfos Defining your process layer palette (the primary approach)
LayerEnum When you need layers to behave as plain integers (KLayout-native style)
LayerStack 3-D simulation / cross-section metadata (thickness, material, z-position)

This page walks through each.

import kfactory as kf
from kfactory.layer import LayerLevel, layerenum_from_dict

LayerInfos — define your layer palette

LayerInfos is a Pydantic model. Subclass it and declare each layer as a class attribute typed kf.kdb.LayerInfo:

class LAYER(kf.LayerInfos):
    WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)  # waveguide core
    WGEX: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2, 0)  # waveguide exclusion zone
    CLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(4, 0)  # cladding
    FLOORPLAN: kf.kdb.LayerInfo = kf.kdb.LayerInfo(10, 0)


L = LAYER()

Each field is a kdb.LayerInfo(layer_number, datatype). The field name is automatically stored as the layer's .name attribute, which is useful for DRC reports and technology files.

Instantiating the class runs Pydantic validation — it checks that every field is a kdb.LayerInfo with valid layer and datatype numbers.

print(L.WG)  # LayerInfo(1/0) — KLayout's string representation
print(L.WG.layer)  # 1
print(L.WG.datatype)  # 0
print(L.WG.name)  # "WG"  — auto-set from the field name
WG (1/0)
1
0
WG

Registering layers with a layout

A KCLayout (the global kf.kcl by default) must know about your layers so that find_layer and other helpers work correctly. Assign your LayerInfos instance to kcl.infos:

kf.kcl.infos = L

Looking up the integer layer index

KLayout stores shapes using an integer layer index (not the (layer, datatype) pair directly). Use kcl.find_layer to convert a LayerInfo to this index:

idx_wg = kf.kcl.find_layer(L.WG)
print(f"WG layer index: {idx_wg}")

# You can also look up by number/datatype directly:
idx_wg2 = kf.kcl.find_layer(1, 0)
print(f"Same index via (layer, datatype): {idx_wg2}")
print(f"Indices match: {idx_wg == idx_wg2}")
WG layer index: WG
Same index via (layer, datatype): WG
Indices match: True

Accessing layers by name

LayerInfos supports dict-style access, which is useful in generic code:

print(L["CLAD"])  # same as L.CLAD
CLAD (4/0)

Iterating over all layers

Because LayerInfos is a Pydantic model, model_fields gives you the declared layers and model_dump() serialises them:

for name in L.model_fields:
    li = getattr(L, name)
    print(f"  {name:12s}  layer={li.layer}  datatype={li.datatype}")
  WG            layer=1  datatype=0
  WGEX          layer=2  datatype=0
  CLAD          layer=4  datatype=0
  FLOORPLAN     layer=10  datatype=0


/tmp/ipykernel_4136/971822332.py:1: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.
  for name in L.model_fields:

LayerEnum — integer-style layer access

LayerEnum is an alternative that maps layer names to KLayout layer indices (integers). It is useful when interfacing with older KLayout APIs that expect an integer directly, or when you want to use a layer as a dict key with O(1) lookup.

Use layerenum_from_dict to convert a LayerInfos into a LayerEnum:

LE = layerenum_from_dict(L)

print(type(LE.WG))  # <enum 'LAYER'>
print(int(LE.WG))  # integer layer index in kf.kcl.layout
print(LE.WG.layer)  # 1  — original layer number
print(LE.WG.datatype)  # 0  — original datatype
print(LE.WG[0], LE.WG[1])  # tuple-style access: (layer, datatype)
<aenum 'LAYER'>
0
1
0
1 0

Both LayerInfos and LayerEnum are valid everywhere kfactory expects a layer — kf.kcl.find_layer accepts a LayerInfo, a LayerEnum, or an (int, int) tuple.

# LayerInfos → find_layer gives the integer index:
print(kf.kcl.find_layer(L.WG))  # from LayerInfos
print(kf.kcl.find_layer(1, 0))  # from (layer, datatype) pair

# LayerEnum members *are* layer indices already:
print(int(LE.WG))  # same integer, no find_layer needed
WG
WG
0

LayerStack — 3-D process metadata

LayerStack stores per-layer physical properties needed for 3-D simulation, cross- section rendering, or fabrication export. Each entry is a LayerLevel:

stack = kf.LayerStack(
    wg_core=LayerLevel(
        layer=L.WG,
        zmin=0.0,
        thickness=0.22,
        material="Si",
        sidewall_angle=85.0,
    ),
    cladding=LayerLevel(
        layer=L.CLAD,
        zmin=-3.0,
        thickness=3.22,
        material="SiO2",
    ),
)

LayerLevel fields:

Field Type Meaning
layer (int, int) or kdb.LayerInfo GDS layer
zmin float µm Bottom of the material
thickness float µm Material thickness
material str | None Material name (for simulation)
sidewall_angle float degrees Etch sidewall angle (90° = vertical)
info Info Free-form simulation metadata
# Access individual levels by attribute or dict key
print(stack["wg_core"].thickness)  # 0.22
print(stack.cladding.material)  # SiO2

# Convenience helpers for simulation
print(stack.get_layer_to_thickness())  # {(1,0): 0.22, (4,0): 3.22}
print(stack.get_layer_to_material())  # {(1,0): 'Si', (4,0): 'SiO2'}
0.22
SiO2
{(1, 0): 0.22, (4, 0): 3.22}
{(1, 0): 'Si', (4, 0): 'SiO2'}

Putting it all together: a minimal PDK layer set

A typical PDK definition combines LayerInfos and LayerStack in one module:

class PDK_LAYER(kf.LayerInfos):
    WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)
    WG_TRENCH: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2, 0)
    METAL1: kf.kdb.LayerInfo = kf.kdb.LayerInfo(11, 0)
    METAL2: kf.kdb.LayerInfo = kf.kdb.LayerInfo(12, 0)
    FLOORPLAN: kf.kdb.LayerInfo = kf.kdb.LayerInfo(99, 0)


pdk_layers = PDK_LAYER()

pdk_stack = kf.LayerStack(
    wg=LayerLevel(layer=pdk_layers.WG, zmin=0.0, thickness=0.22, material="Si"),
    m1=LayerLevel(layer=pdk_layers.METAL1, zmin=0.5, thickness=0.5, material="Al"),
    m2=LayerLevel(layer=pdk_layers.METAL2, zmin=1.2, thickness=0.5, material="Al"),
)

print("Layer palette:")
for name in pdk_layers.model_fields:
    li = getattr(pdk_layers, name)
    print(f"  {name:12s}  ({li.layer}/{li.datatype})")

print("\n3-D stack:")
for name, level in pdk_stack.layers.items():
    print(
        f"  {name:6s}  z={level.zmin:.1f}{level.zmin + level.thickness:.2f} µm  {level.material}"
    )
Layer palette:
  WG            (1/0)
  WG_TRENCH     (2/0)
  METAL1        (11/0)
  METAL2        (12/0)
  FLOORPLAN     (99/0)

3-D stack:
  wg      z=0.0…0.22 µm  Si
  m1      z=0.5…1.00 µm  Al
  m2      z=1.2…1.70 µm  Al


/tmp/ipykernel_4136/3302201102.py:18: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.
  for name in pdk_layers.model_fields:

See Also

Topic Where
KCLayout — the layout registry that owns layers Core Concepts: KCLayout
Cross-sections built on top of layers Cross-Sections
LayerLevel and full 3-D stack in a PDK PDK: Technology & Layer Stack
Assembling a full PDK with layers PDK: Creating a PDK