Skip to content

Building and Managing PDKs

This guide covers strategies for organizing PDK repositories and managing tapeouts over time. Choosing the right structure early prevents maintenance headaches as your team scales across multiple tapeouts.

The problem

A PDK is a living codebase: it evolves with each tapeout, but completed tapeouts must stay frozen so they remain reproducible. Balancing evolution with reproducibility is the central challenge.

Repository structures

There are three common approaches, each with different tradeoffs.

1. Mutable PDK in monorepo (worst option)

A single repository where the PDK and each tapeout live as sibling directories, with the PDK as the shared, evolving source of truth.

pdk-repo/
    pdk/                # Plan of record (evolves)
    tapeout1/
        tapeout1_files/
    tapeout2/
        tapeout2_files/

Use branches or tags (_v1, _v2, _v3) to snapshot PDK state for each tapeout.

Pros:

  • Single location for all files — easy to tell people where to look.

Cons:

  • The repo becomes a monolith that is hard to maintain.
  • Completed tapeouts can break when the PDK evolves.
  • Many open issues, pull requests and branches accumulate, making the repo hard to navigate and archive.
  • Makes PDK management and reproducibility difficult, especially if you start adding new layers or experimental components.
  • Hard to update dependencies of the tools used in the PDK (e.g., gdsfactory, SAX, etc.) without breaking existing tapeouts.

2. New PDK per tapeout in monorepo

Similar to the complex monorepo, but each tapeout directory includes its own frozen copy of the PDK.

pdk-repo/
    pdk/                    # Plan of record (evolves)
    tapeout1/
        pdk/                # Frozen after tapeout1 is done
        tapeout1_files/
    tapeout2/
        pdk/                # May diverge from tapeout1's copy
        tapeout2_files/

Pros:

  • Single location for all files.
  • Each tapeout has its own PDK snapshot.

Cons:

  • Tends to get messy over time (many open pull requests and issues).

Each tapeout lives in its own repository with an independent copy of the PDK. A shared CI/CD repo provides common tooling.

repo1: tapeout1/
    pdk/              # Frozen after tapeout1 is done
    tapeout1_files/
repo2: tapeout2/
    pdk/              # Frozen after tapeout2 is done
    tapeout2_files/
repo3: pdk/           # Plan of record PDK (Optional) Evolves over time

Pros:

  • Completed tapeout repos can be archived and will always work. Easy to reproduce results.
  • Improvements from any tapeout can be selectively copied back into an optional plan-of-record PDK repo.
  • Each repo stays focused and manageable.

Cons:

  • Files are spread across multiple repos. Ideally, someone keeps documentation of the order of the tapeouts.
  • Requires maintenance of the centralized CI/CD repo.
  1. Start from the PDK repo. Develop and iterate on the plan-of-record PDK.
  2. Copy the plan-of-record PDK for each tapeout. When a tapeout begins, create a new repo (e.g., tapeout-<name>) with a copy of the current PDK.
  3. Freeze on completion. After the tapeout is done, archive the repo. The frozen PDK and locked dependencies ensure reproducibility. You can still use tags and releases to add comments and metadata (like sent to the foundry, reworked metallization, etc.).
  4. Backport improvements. Copy relevant improvements from tapeout repos back into the main PDK repo for future tapeouts.
  5. Use XOR regression tests. Each tapeout repo should include regression tests to catch unintended layout changes.

Flavours

A PDK can ship several flavours from one repository: variants of the same process that share a base but differ in which cells and layers they offer. Use this when the variants are the same process, so a fix to a shared cell should reach every flavour at once. Use separate PDKs when they are separate processes.

Structure

One distribution contains a base package and one package per flavour. Every cell is defined once, in the base. Each flavour builds its PDK from the base's PDK factory, with its own name and layers, and the base cell registry minus the cells it removes:

my_pdk/            # base: every cell, plus get_pdk(name, layers, cells_dict)
my_pdk_b/          # flavour: PDK = get_pdk("my_pdk_b", LAYER_B, base cells minus a few)
my_pdk_c/          # flavour: PDK = get_pdk("my_pdk_c", LAYER_C, base cells minus others)
# my_pdk_c/__init__.py
from my_pdk.pdk import _get_cells, get_pdk
from my_pdk_c.layers import LAYER, LAYER_STACK, LAYER_VIEWS

cells = _get_cells()
for name in ("bump_elec", "bump_mech"):
    cells.pop(name, None)

PDK = get_pdk(
    name="my_pdk_c",
    layers=LAYER,
    layer_stack=LAYER_STACK,
    layer_views=LAYER_VIEWS,
    cells_dict=cells,
)

Declaring the family

A project picks its flavour with name, and lists the whole family in options. The Select PDK command offers the same list:

[tool.gdsfactoryplus.pdk]
name = "my_pdk_c"
options = ["my_pdk", "my_pdk_b", "my_pdk_c"]

Entries in options are PDK module paths and may be nested, such as an350.cband and an350.oband. The same declaration works for independent sub-PDKs, such as CSPDK's cspdk.si220.cband and cspdk.si500, which each define their own cells.

What GDSFactory+ does

GDSFactory+ reads the selected PDK's PDK.cells at runtime and uses it only to decide what is offered:

  • Cells they own that the selected PDK doesn't provide are hidden from the Livewire and Mosaic component pickers and from the sidebar. Examples are bump_elec in a ph18dac project, or every cspdk.si500 cell in a cspdk.si220.cband project.
  • Everything else is unchanged. The PDK list is not filtered. A sidebar filter chip whose cells are all hidden simply disappears. A shared base cell the flavour keeps is still listed under the base package, and built in the base PDK's worker. A hidden cell still builds if something references it. Project cells and cells from other PDKs are never hidden.
  • It fails open. Until the registry has been read, or if reading it fails (for example the PDK doesn't import), nothing is hidden and a warning is logged.

If name is not listed in options, GDSFactory+ logs a warning and ignores the family.

Example: PH18DA

PH18DA ships ph18da (the base, with every cell), ph18dab (custom layers, without the bump cells) and ph18dac (custom layers and tech, without 13 cells), all in a single wheel. A ph18dac project declares:

[tool.gdsfactoryplus.pdk]
name = "ph18dac"
options = ["ph18da", "ph18dab", "ph18dac"]

In that project, the cells ph18dac removes, such as bump_elec and gc_oip_2d, are not offered, and the base cells it keeps stay under the ph18da filter chip.

Runtime CONF

Some PDKs switch behaviour through a gf.CONF value instead of separate packages. PH18, for example, selects its process variant (layer stack, metal and grating-coupler data) with gf.CONF.process_variant, which it reads when it is imported. In a project, set these values in pyproject.toml, or in the Runtime CONF editor on the settings page:

[tool.gdsfactoryplus.pdk.conf]
process_variant = "mci"

Every PDK worker applies these values to gf.CONF before it imports the PDK. Changing them restarts the workers. GDSFactory+ does not validate them: an invalid value shows up as the PDK's own error under Worker Health. Parameter defaults that GDSFactory+ reads from source code do not follow these values.

Custom schematic symbols

To ship a custom SVG schematic symbol with a component, set display={"symbol": "mypdk.symbols:ring_modulator.svg"} in the component's metadata. The value is a package:path.svg reference to a resource inside an installed Python package; open schematics draw the component with that artwork.

Naming conventions

Use a consistent naming scheme for tapeout repos:

tapeout-<name>          # e.g., tapeout1, tapeout2, ...

Link tapeout repos from the main PDK repo's README so the full history is easy to navigate.

Extra files to include

repo: tapeout1/
    pdk/                    # Frozen after tapeout1 is done
    tapeout1_files/
    docs/ # documentation for the tapeout, including jupyter notebooks and other design documents
    tests/ # regression tests, with XOR comparisons to catch unintended layout changes
    pyproject.toml # with pinned dependencies. gdsfactory==9.43.0, ...
    uv.lock # with pinned dependencies