## KLayout quick reference and notes ### Creating and Importing PCells PCells, or *parametrized cells*, allow users to create flexible structures that can be easily modified or reused without going through a complex manual drawing process. According to the official documentation: > Starting with version 0.22, KLayout offers parametrized cells (PCells). PCells are a feature found in other tools to simplify layout by providing generators for common layout building blocks. Parametrized cells do not contain static layout but are created dynamically by code using a given set of parameters. > > See [About PCells](https://www.klayout.de/doc/about/about_pcells.html) #### Import PCells Open the **Macros Development** window by selecting `Macros → Macros Development` from the top menu. You should see a window similar to the one below: ![[Open_UI.png]] Next, switch to the Python interpreter by clicking **Python** in the left tab. Under **[Local]**, select or create a new directory (`New folder`), then choose **Import file** to add scripts into this folder. ![[Import_ref.jpg]] The content of the `register` script is shown below. This script searches for and registers all existing scripts in the same directory when KLayout starts, so that the PCells appear directly in the library. Remember to update the lines `lib = pya.Library.library_by_name("your_lib_name")` and `lib.register("your_lib_name")` to the desired name for your library. >[!code]- > This is the `register.py` file. > ```python > # $autorun > # register.py — set this one to Auto run > import pya, sys, os, importlib, inspect, glob > # 0) Basic paths > BASE_DIR = os.path.dirname(__file__) # .../EtchLib > PKG_NAME = os.path.basename(BASE_DIR) # "EtchLib" > PARENT = os.path.dirname(BASE_DIR) > if PARENT not in sys.path: > sys.path.append(PARENT) > print(f"[register] PACKAGE={PKG_NAME}") > print(f"[register] BASE_DIR={BASE_DIR}") > > # 1) List .py files we plan to import (exclude register/__init__) > py_files = [] > for f in glob.glob(os.path.join(BASE_DIR, "*.py")): > stem = os.path.splitext(os.path.basename(f))[0] > if stem in ("register", "__init__"): > continue > py_files.append(stem) > print(f"[register] candidates: {py_files}") > > # 2) Get or create library > lib = pya.Library.library_by_name("your_lib_name") > # Change here the Lib name you prefer > print(f"[register] existing lib? -> {lib is not None}") > if lib is None: > lib = pya.Library() > lib.description = "Etching test PCells (stripe arrays, hole/pillar arrays)" > > # 3) Import each module and register any PCell classes found > found = 0 > for stem in py_files: > mod_qual = f"{PKG_NAME}.{stem}" > try: > module = importlib.import_module(mod_qual) > print(f"[register] imported {mod_qual}") > except Exception as e: > print(f"[register] FAILED import {mod_qual}: {e}") > continue > > # find subclasses > cls_found = 0 > for _, cls in inspect.getmembers(module, inspect.isclass) > if issubclass(cls, pya.PCellDeclarationHelper) and cls is not pya.PCellDeclarationHelper: > display_name = getattr(module, "DISPLAY_NAME", > getattr(cls, "DISPLAY_NAME", cls.__name__)) > lib.layout().register_pcell(display_name, cls()) > print(f"[register] registered: {display_name} ({cls.__name__})") > found += 1 > cls_found += 1 > if cls_found == 0: > print(f"[register] NOTE: no PCell classes found in {mod_qual}") > > # 4) Finalize/update library (Python API needs the name argument) > lib.register("your_lib_name") > print(f"[register] DONE. EtchLib now has at least {found} PCell(s).") > ``` If you double-click `register` in the directory and then click **Run script from the current tab** at the top, the script will execute and display the base directory of these macros. This makes it possible to open the directory directly, modify the files with other editors, or copy additional files into the same folder without using the `Import` function. ![[base_dir_ill.jpg]] #### Creating PCell Scripts Creating new scripts is straightforward. Click `+` (**New**), then select the directory where you want to add the script. In the popup window, make sure to choose **Plain Python file (*.py)** if you want to edit or test the script with another Python interpreter and use it together with `register.py`. Other options, such as **General KLayout macro (*.lym)** or **PCell sample (Python)**, will not create a `.py` file but instead a `.lym` file. >[!Note] >It is recommended to use `.lym` scripts for testing and debugging. Once they work, convert them into `.py` format for compatibility with `register.py`. #### Writing PCell Scripts A typical Python PCell script in KLayout follows a standard structure. It usually consists of the following parts: file preamble, class skeleton, parameter declarations, parameter coercion, and geometry generation. The `StripeArrayEtch` example below illustrates this organization. >[!Notice] >The code shown here was designed for personal use and to work with `register.py`. Although it follows the `PCell sample` template and is organized in a similar way, it is not necessarily best practice and could be improved. For official recommendations, please refer to the KLayout manual. **1. File preamble** At the top of the file, import the required libraries such as `pya` (KLayout API) and standard modules like `math`. Optionally, define a module-level `DISPLAY_NAME` so that `register.py` can use it as the library display name. ```python import pya import math DISPLAY_NAME = "StripeArrayEtch" ``` **2. Class skeleton** Define the PCell as a subclass of `pya.PCellDeclarationHelper`. Within the constructor, call `super().__init__()` and declare all necessary parameters. ```python class StripeArrayEtch(pya.PCellDeclarationHelper): def __init__(self): super().__init__() # parameter declarations go here ``` **3. Parameter declarations** Parameter declarations define the interface of the PCell and determine the options shown in the GUI. They specify what the user can edit, what default values are provided, and how values are represented. Typical categories include: - **Layer parameters**: define which layer or datatype the geometry is placed on. - **Geometric inputs**: key values such as lengths, widths, radii, gaps, pitches, or angles that directly shape the generated geometry. - **Constraints and limits**: minimum/maximum values, read-only caps, or other bounds that keep the geometry valid. - **Choice lists**: drop-down menus for selecting among discrete options such as orientation, symmetry, or shape type. - **Derived read-only parameters**: automatically updated values such as total span, area, or count that reflect the current configuration but cannot be edited. - **Optional or hidden parameters**: advanced settings that may be hidden from the default UI or only exposed when needed. Together, these parameters define both the **user-facing controls** and the **internal data model** of the PCell, making clear how inputs map to the generated geometry. ```python self.param("l", self.TypeLayer, "Layer", default=pya.LayerInfo(1, 0)) self.param("L", self.TypeDouble, "Base length (µm)", default=10.0) self.param("dL", self.TypeDouble, "Length increment (µm)", default=0.0) self.param("N", self.TypeInt, "Number of stripes", default=5) self.param("Lmin", self.TypeDouble, "Minimum stripe length (µm)", default=0.1) self.param("Nmax", self.TypeInt, "Max stripes by length", readonly=True, default=10) self.param("theta", self.TypeList, "Orientation (deg)", choices=[("0°","0"),("90°","90"),("180°","180"),("270°","270")], default="0") self.param("anchor", self.TypeList, "Anchor", choices=[("edge (from one side)","edge"), ("center (symmetrized)","center")], default="edge") self.param("span", self.TypeDouble, "Total span (µm)", readonly=True, default=0.0) ``` **4. Instance label** Use `display_text_impl` to provide a short, human-readable string that appears in the instance browser. ```python def display_text_impl(self): return f"StripeArrayEtch(N={self.N}/{self.Nmax}, L={self.L:.2f}µm, dL={self.dL:.2f}, theta={self.theta}°)" ``` **5. Parameter coercion** `coerce_parameters_impl` is responsible for validating user input and ensuring that all parameters are in a consistent, safe state before geometry generation. This step corrects invalid or conflicting values and updates any dependent or derived parameters. Typical tasks in this stage include: - **Sanitizing input values**: rounding integers, clamping negative or zero values, and checking ranges. - **Enforcing design rules**: applying minimum/maximum limits, preventing degenerate geometry, and ensuring spacing constraints. - **Maintaining parameter consistency**: adjusting one parameter when another changes (e.g., limiting the number of stripes when a progression would otherwise produce invalid lengths). - **Computing derived values**: calculating total area, span, pitch, bounding boxes, or any read-only parameters shown in the UI. - **Preparing geometry data**: precomputing arrays of widths, gaps, angles, or positions that will later be used in `produce_impl`. In general, `coerce_parameters_impl` acts as both a safeguard and a pre-processor: it converts freeform user input into reliable, internally consistent parameters for geometry generation. ```python def coerce_parameters_impl(self): self.N = max(1, int(round(self.N))) self.Lmin = max(self.Lmin, 0.1) if self.L <= 0: self.L = self.Lmin if self.W0 <= 0: self.W0 = 0.1 if self.dL < 0: nmax = int(math.floor((self.L - self.Lmin)/(-self.dL))) + 1 if self.L > self.Lmin else 1 nmax = max(1, nmax) if self.N > nmax: self.N = nmax self.Nmax = nmax else: self.Nmax = max(1, self.N) widths = [max(self.W0 + i*self.dW, 0.001) for i in range(self.N)] gaps = [max(self.G0 + i*self.dG, 0.0) for i in range(max(0, self.N-1))] self.span = widths[0] if self.N==1 else (widths[0]/2 + sum(gaps) + sum(widths[1:-1]) + widths[-1]/2) ``` **6. Geometry generation** `produce_impl` is the method where the actual layout geometry is created and inserted into the target cell. The exact implementation depends on the type of structure, but it typically follows these steps: - Interpret the sanitized parameter values and derive any additional quantities (e.g., dimensions, offsets, orientations). - Build the geometric primitives that represent the structure, such as: - **Boxes/rectangles** (`DBox`) for stripes or pads - **Polygons/regions** for arbitrary shapes - **Paths** (`DPath`) for waveguides or interconnects - **Circles/ellipses** (approximated by polygons) for holes or disks - Apply translations, rotations, or scaling using `DCplxTrans` or related transforms. - Insert the transformed shapes into `self.cell.shapes(self.l_layer)` so they become part of the PCell instance. This general workflow—**compute derived geometry → build shapes → transform → insert**—applies regardless of the specific primitives used. ```python def produce_impl(self): N = int(round(self.N)) L0, dL, Lmin = float(self.L), float(self.dL), float(self.Lmin) widths = [max(self.W0 + i*self.dW, 0.001) for i in range(N)] gaps = [max(self.G0 + i*self.dG, 0.0) for i in range(max(0, N-1))] span = widths[0] if N==1 else (widths[0]/2 + sum(gaps) + sum(widths[1:-1]) + widths[-1]/2) o = (-span/2 + widths[0]/2) if self.anchor == "center" else (widths[0]/2) offsets = [o] for i in range(1, N): o += widths[i-1]/2 + gaps[i-1] + widths[i]/2 offsets.append(o) theta_deg = float(self.theta) theta_rad = math.radians(theta_deg) nx, ny = -math.sin(theta_rad), math.cos(theta_rad) lidx = self.l_layer for i, (w, oi) in enumerate(zip(widths, offsets)): Li = max(L0 + i*dL, Lmin) box = pya.DBox(-Li/2.0, -w/2.0, +Li/2.0, +w/2.0) cx, cy = oi*nx, oi*ny xform = pya.DCplxTrans(1.0, theta_deg, False, cx, cy) self.cell.shapes(lidx).insert(box.transformed(xform)) ``` **7. Additional notes** - Use the **D-API** (`DBox`, `DPoint`, `DCplxTrans`) to work directly in micrometers. - `TypeLayer` parameters provide `self.l_layer`, which corresponds to the numeric layer index. - Keep the script structure clear by separating the stages: **parameter declarations → coercion → geometry**. This organization pattern—declare parameters, coerce values, then generate geometry—is the standard approach to writing Python PCells in KLayout. The `StripeArrayEtch` class provides a complete example of this structure. >[!code]- > > ```python > import pya > > import math > > > > DISPLAY_NAME = "StripeArrayEtch" > > # --------------------------------------------- > > # StripeArrayEtch PCell (µm units, polygon boxes) > > # - Angle is a dropdown (0/90/180/270 deg) > > # - Length supports arithmetic progression via dL > > # --------------------------------------------- > > class StripeArrayEtch(pya.PCellDeclarationHelper): > > > > def __init__(self): > > super(StripeArrayEtch, self).__init__() > > > > # ===== Basic parameters ===== > > self.param("l", self.TypeLayer, "Layer", default=pya.LayerInfo(1, 0)) # target layer > > self.param("L", self.TypeDouble, "Base length (µm)", default=10.0) # base length for stripe i=0 > > self.param("dL", self.TypeDouble, "Length increment (µm)", default=0.0) # arithmetic increment per stripe > > self.param("W0", self.TypeDouble, "Initial width (µm)", default=0.5) # width for stripe i=0 > > self.param("dW", self.TypeDouble, "Width increment (µm)", default=0.0) # arithmetic increment per stripe > > self.param("G0", self.TypeDouble, "Initial gap (µm)", default=0.5) # gap between stripe 0 and 1 > > self.param("dG", self.TypeDouble, "Gap increment (µm)", default=0.0) # arithmetic increment per gap > > self.param("N", self.TypeInt, "Number of stripes", default=5) # total stripes > > # Minimum allowed length and computed cap on stripes > > self.param("Lmin", self.TypeDouble, "Minimum stripe length (µm)", default=0.1) > > self.param("Nmax", self.TypeInt, "Max stripes by length", readonly=True, default=10) > > > > # Angle as dropdown (stored as string; convert to float in produce_impl) > > self.param( > > "theta", self.TypeList, "Orientation (deg)", > > choices=[("0°", "0"), ("90°", "90"), ("180°", "180"), ("270°", "270")], > > default="0" > > ) > > > > # ===== Optional / read-only ===== > > self.param( > > "anchor", self.TypeList, "Anchor", > > choices=[("edge (from one side)", "edge"), > > ("center (symmetrized)", "center")], > > default="edge" > > ) > > self.param("span", self.TypeDouble, "Total span (µm)", readonly=True, default=0.0) > > > > # Shown in the instance browser > > def display_text_impl(self): > > return (f"StripeArrayEtch(N={self.N}/{self.Nmax}, " > > f"L={self.L:.2f}µm, dL={self.dL:.2f}, theta={self.theta}°)") > > > > # Sanity checks and derived values > > def coerce_parameters_impl(self): > > import math > > # ---- basic sanity ---- > > self.N = int(round(self.N)) > > if self.N < 1: > > self.N = 1 > > if self.Lmin <= 0: > > self.Lmin = 0.1 > > if self.L <= 0: > > self.L = self.Lmin > > if self.W0 <= 0: > > self.W0 = 0.1 > > # ---- clamp N against length progression when dL < 0 ---- > > # Length(i) = L + i*dL must stay >= Lmin > > if self.dL < 0: > > if self.L <= self.Lmin: > > nmax = 1 > > else: > > # Find largest i such that L + i*dL >= Lmin -> i <= (L - Lmin)/(-dL) > > nmax = int(math.floor((self.L - self.Lmin) / (-self.dL))) + 1 > > nmax = max(1, nmax) > > if self.N > nmax: > > self.N = nmax > > self.Nmax = nmax > > else: > > # No upper cap from length when dL >= 0 (unless you later add an Lmax) > > self.Nmax = max(1, self.N) > > # ---- widths and gaps (clamped non-negative) ---- > > widths = [self.W0 + i * self.dW for i in range(self.N)] > > gaps = [self.G0 + i * self.dG for i in range(max(0, self.N - 1))] > > widths = [max(w, 0.001) for w in widths] # >= 1 nm to avoid degenerate boxes > > gaps = [max(g, 0.0) for g in gaps] > > # ---- span along array normal (independent of lengths) ---- > > if self.N == 1: > > span = widths[0] > > else: > > span = widths[0]/2 + sum(gaps) + sum(widths[1:-1]) + widths[-1]/2 > > self.span = span > > > > > > # Geometry generation > > def produce_impl(self): > > N = int(round(self.N)) > > L0 = float(self.L) > > dL = float(self.dL) > > Lmin = float(self.Lmin) > > > > # Series for widths/gaps > > widths = [max(self.W0 + i * self.dW, 0.001) for i in range(N)] > > gaps = [max(self.G0 + i * self.dG, 0.0) for i in range(max(0, N - 1))] > > > > # Span (same formula as in coerce) > > if N == 1: > > span = widths[0] > > else: > > span = widths[0]/2 + sum(gaps) + sum(widths[1:-1]) + widths[-1]/2 > > > > # Offsets along array normal (center positions for each stripe) > > if self.anchor == "center": > > o = -span/2 + widths[0]/2 > > else: # "edge" > > o = widths[0]/2 > > offsets = [o] > > for i in range(1, N): > > o += widths[i-1]/2 + gaps[i-1] + widths[i]/2 > > offsets.append(o) > > > > # Orientation: dropdown gives string ("0","90","180","270") > > theta_deg = float(self.theta) > > theta_rad = math.radians(theta_deg) > > > > # Normal unit vector (array stacking direction) > > nx, ny = -math.sin(theta_rad), math.cos(theta_rad) > > > > lidx = self.l_layer > > for i, (w, oi) in enumerate(zip(widths, offsets)): > > # Per-stripe length with arithmetic progression > > Li = max(L0 + i * dL, Lmin) # clamp positive > > > > # Local box centered at origin: x∈[-Li/2, +Li/2], y∈[-w/2, +w/2] > > box = pya.DBox(-Li/2.0, -w/2.0, +Li/2.0, +w/2.0) > > > > # Translate along normal, then rotate by theta > > cx, cy = oi * nx, oi * ny > > xform = pya.DCplxTrans(1.0, theta_deg, False, cx, cy) > > self.cell.shapes(lidx).insert(box.transformed(xform)) > ``` #### Using PCells Imported or newly created PCells can be found in the **Libraries** menu. Simply drag a structure into the desired layer to create an instance, and adjust its parameters as needed. ![[Lib_illi_Klayout.png]] >[!Note] >If a PCell does not appear, try selecting `Display → Full Hierarchy` to make it visible. ### Boolean operations for layers