---
myst:
html_meta:
description: How to fork QPDK into a process-specific PDK and keep the fork current with upstream releases.
---
# Customizing QPDK for Your Process
QPDK describes a generic superconducting process. For a real fabrication process, you need your own PDK with your own
layers, layer stack, and cells. You also want to keep getting fixes and new features from QPDK. This page shows how to
do both.
In short:
1. Fork the repository and rename it in one commit.
1. Replace the process (`qpdk/layers.yaml` and `qpdk/tech.py`) in one more commit.
1. Remove or override cells, then regenerate the regression references.
1. Merge new QPDK release tags often. Use the [ownership map](#ownership-map) to resolve conflicts.
## Fork or compose
You can build on QPDK in two ways:
- **Fork.** Copy the repository, own the process, and merge new QPDK changes into it over time.
- **Compose.** Keep `qpdk` as a dependency. Build your own `gdsfactory.Pdk` in a separate package that reuses QPDK cells
and models.
**Fork if you change the layers, the layer stack, or the materials.** Almost all QPDK code reads the process from
`qpdk.tech`. Cells draw on `LAYER.*`, cross-sections are defined there, and the CPW models read thicknesses from
`LAYER_STACK` (see `qpdk/models/cpw.py`). A separate package cannot change these without patching QPDK internals.
**Compose if you only add things.** If the QPDK process is close enough, depend on `qpdk` and add your cells to its
registries, for example `cells={**qpdk.PDK.cells, **my_cells}`. You never merge anything. You upgrade by changing the
version. Issue [#376](https://github.com/gdsfactory/quantum-rf-pdk/issues/376) tracks a project template for this.
```{mermaid}
:alt: If you change the layers, layer stack, or materials, fork. If you only add cells, samples, or chips, compose.
flowchart LR
Q1["New layers, stack,
or materials?"] -->|yes| F["Fork:
own the process"]
Q1 -->|no| Q2["Only adding cells,
samples, or chips?"]
Q2 -->|no| F
Q2 -->|yes| C["Compose:
depend on qpdk"]
```
The rest of this page is about forks.
(setting-up-the-fork)=
## Setting up the fork
Create the fork on your Git host. Then add QPDK as a second remote. Here `origin` is your fork and `upstream` is QPDK.
```bash
git clone
cd
git remote add upstream https://github.com/gdsfactory/quantum-rf-pdk.git
# Your releases also create v tags.
# Fetch the QPDK tags as upstream/v so the two do not collide.
git config remote.upstream.tagOpt --no-tags
git config --add remote.upstream.fetch '+refs/tags/*:refs/tags/upstream/*'
git fetch upstream
git tag -l 'upstream/*' # upstream/v0.4.0, upstream/v0.4.1, ...
# Let Git reuse your earlier conflict resolutions.
# See https://git-scm.com/book/en/v2/Git-Tools-Rerere
git config rerere.enabled true
```
Work on branches and merge them into `main` through pull requests. Do not work on `main` directly. It is the branch you
merge QPDK into.
(identity-rename)=
## Identity rename
The places below contain the QPDK name. Change all of them in **one commit** and change nothing else in it. Later merges
then conflict here in a known way, and the diff of this commit is your checklist.
| What | Where |
| ------------------- | -------------------------------------------------------------------------------------- |
| Package directory | `qpdk/`, and every `import qpdk` / `from qpdk` (`git grep -l qpdk`: most of the repo) |
| Name and version | `[project]` in `pyproject.toml`, `__version__` in `qpdk/__init__.py`, `[tool.tbump]` |
| Tooling paths | `pyproject.toml`: coverage `source`, pytest `testpaths`, ruff per-file ignores |
| GDSFactory+ project | `pyproject.toml`: `[tool.gdsfactoryplus]` `name`, `pdk.name`, `sidebar` libraries |
| Layer map class | `LayerMapQPDK` in `qpdk/tech.py` |
| PDK object | `name="qpdk"` in `get_pdk()`, and the `QPdk` / `_QPdkMeta` classes, `qpdk/__init__.py` |
| KLayout technology | `` in `qpdk/klayout/tech.lyt`, `KLayoutTechnology(name=...)` in `qpdk/tech.py` |
| KLayout install | `src` and `dest` in `qpdk/install_tech.py` |
| Docs | `docs/conf.py`: `project`, logos, URLs, and badges in README and index |
| Workflows | `test.yml` (`--cov=qpdk`, imports) |
| Repo metadata | Repository URLs in `.github/` (issue templates, release drafter, lychee), `AGENTS.md` |
The `justfile` reads the package name from `pdk := env('pdk', 'qpdk')`. Set `pdk=` in `.env` or change the
default. Only the `install-tech` and `show` recipes read this variable.
Internal names that users never see can stay, for example `docs/_ext/qpdk_*.py` and the `qpdk.mplstyle` figure style.
Each name you keep is one less line that can conflict.
## Process replacement
The process is defined in two files, and they must agree:
- `qpdk/layers.yaml`: layer names, GDS numbers, and how KLayout draws them.
- `qpdk/tech.py`: everything else.
`tests/test_pdk.py::test_yaml_matches_layers` fails if the two files have different layer names or `(layer, datatype)`
pairs. After you edit `layers.yaml`, regenerate the KLayout files and commit them too:
```bash
uv run python qpdk/tech.py # rewrites qpdk/klayout/layers.lyp and tech.lyt
```
QPDK reads `LAYER_VIEWS` from the generated `layers.lyp`, not from the YAML. If you skip this step, the test passes but
KLayout and GDSFactory+ still show the old layers. The generated files differ from the committed ones even if you
changed nothing. For example, `` entries change order and layer names lose their `1/0` suffix. Check the diff
and keep only your changes.
What a foundry PDK usually changes in `qpdk/tech.py`:
- **`LayerMapQPDK`**: the GDS layer numbers and any extra mask layers. Keep the non-fabrication layers (`SIM_AREA`,
`SIM_ONLY`, `LABEL_*`, `WG`, `ERROR_PATH`). Cells, routing, and simulation use them.
- **`get_layer_stack()`**: thicknesses, `zmin`, and materials, for example Al instead of Nb. Keep the level names `M1`,
`Substrate`, and `Vacuum`. The CPW models and `LAYER_STACK_NO_VACUUM` look them up by name.
- **`material_properties`**: permittivity and loss tangent of your substrate. The CPW models read these.
- **`LAYER_CONNECTIVITY`**: which layers connect through vias, bumps, and bridges.
- **Cross-sections** such as `coplanar_waveguide`: default width, gap, and bend radius for 50 Ω on your substrate.
- **`routing_strategies`**: the default cross-sections for routing.
QPDK has no table of design rules. Minimum widths and gaps are default parameter values in the cross-sections and cells.
Check the cells you keep against your rules. For hosted DRC, set `[tool.gdsfactoryplus.drc] pdk` in `pyproject.toml` to
your own rule deck.
The FEM datasets in `qpdk/models/datasets/` were simulated on the QPDK stack. The analytical models follow your stack,
but these datasets do not. Regenerate them with the scripts in `qpdk/models/datasets/data/`, or treat the models that
use them as approximate.
(cell-library)=
## Cell library
`qpdk/cells/__init__.py` star-imports every cell module. `get_pdk()` then collects every public function decorated with
`@gf.cell` or annotated to return a `Component`. A cell is registered under its **name in `qpdk.cells`**. So:
- **To add a cell**, write it in a new module in `qpdk/cells/`, decorate it with `@gf.cell`, and star-import the module
in `qpdk/cells/__init__.py`. Give it a prefix, for example `acme_transmon`, so it never clashes with a future QPDK
cell.
- **To override a cell**, define a function with the same name in a module that is star-imported **after** the QPDK one.
The last import wins. YAML netlists, `gf.get_component("name")`, and GDSFactory+ then use your version. QPDK cells
that import the original directly still use the original. For example, `qpdk/cells/capacitor.py` imports `straight`
directly, so it ignores your `straight`.
Your version and the original also produce different cells with the **same name**, and both can end up in one layout.
So only override cells that no other cell imports. Otherwise give your version a new name, such as `acme_straight`,
and update the callers. Use the same `tags=` as the original cell.
- **To remove a cell**, delete its import or its function. Then delete its tests and regression references. Several
tests import cells directly.
Before you remove a cell, check which cells import it, for example with `git grep -n "import.*\bjosephson_junction\b"`.
SAX models are also matched to cells by name. `SAX_MODEL_ALIASES` in `qpdk/__init__.py` lets some cells use another
cell's model. If you remove a model that an alias points to, `import qpdk` fails with a `KeyError`. If you remove a cell
that has an alias, remove the alias too.
## Pruning
These parts describe the generic QPDK process. Remove or replace them:
- `qpdk/samples/` and the test chips imported at the top of `qpdk/cells/__init__.py`
- `notebooks/` and the docs pages that include them (`docs/notebooks.rst`, `docs/simulation.rst`)
- README sections such as the component list, sample chips, and simulation showcase
- Regression references in `tests/gds_ref/`, `tests/test_pdk/`, and `tests/models/test_models_regression/`
When your process and cells are ready, regenerate the references. Check the diff before you commit.
```bash
just test-gds-force # GDS, settings, and netlist references
just test-models-force # model references, if your stack changes model results
just test # everything should pass now
```
## Staying current with upstream
A fork that stops merging QPDK stops getting model fixes, new cells, and tool updates. After a year it is very hard to
catch up. So merge often and keep each merge small.
### Merge release tags, not `main`
QPDK creates a `v` tag each time the version changes on `main`. A tag is a tested state with a changelog. Merge
each new tag, or at least one tag per month.
Your `main` is your own work plus one merge for each QPDK tag:
```{mermaid}
:alt: The fork starts from an upstream tag, adds its own commits, and merges each later tag via a sync pull request.
flowchart LR
U1["upstream
v0.4.0"] --> U2["upstream
v0.4.1"] --> U3["upstream
v0.5.0"]
U1 -->|fork| F1["Rename, process,
pruning"] --> M1["Merge
v0.4.1"]
M1 --> F2["Your
changes"] --> M2["Merge
v0.5.0"]
U2 -->|sync PR| M1
U3 -->|sync PR| M2
```
Before you merge, check what is new and how far you have diverged:
```bash
git fetch upstream
git for-each-ref --sort=-v:refname --count=1 --format='%(refname:short)' 'refs/tags/upstream/v*' # latest tag
git log --oneline upstream/v0.4.0..upstream/v0.4.1 # what the new tag adds
git log --oneline upstream/v0.4.1..main # what your fork adds
git diff --stat upstream/v0.4.1 main # files that differ
```
Read the release notes and the [changelog](changelog.md) for every tag since your last merge, including tags you skip.
Look for renamed cells and changed defaults.
(ownership-map)=
### Ownership map
Each path belongs to your fork, to QPDK, or to both. The table shows what to do when it conflicts.
| Path | Owner | On merge |
| ------------------------------------------------------ | ----- | ------------------------------------------------ |
| [Identity files](#identity-rename), version | Fork | Keep your names. Take the other QPDK changes |
| `qpdk/layers.yaml`, `qpdk/klayout/` | Fork | Keep yours. Add QPDK layers you want, regenerate |
| `qpdk/tech.py` | Both | Keep your values. Take new QPDK definitions |
| `qpdk/models/` (except datasets) | QPDK | Take QPDK |
| `qpdk/models/datasets/data/` | QPDK | Take QPDK, unless you regenerated it |
| QPDK modules in `qpdk/cells/` | QPDK | Merge. New QPDK cells usually merge cleanly |
| Your cell modules, override modules, and test chips | Fork | QPDK never changes them |
| `qpdk/samples/`, `notebooks/` | Both | Keep removed files removed. Take QPDK for others |
| `tests/gds_ref/`, `tests/test_pdk/`, model `.npz` refs | Fork | Never merge. Keep yours, then regenerate |
| Other tests, `justfile`, `*.just` | QPDK | Take QPDK |
| `docs/conf.py`, `docs/index.md`, `README.md` | Both | Keep your names. Take structure changes |
| `.github/workflows/`, `.pre-commit-config.yaml` | QPDK | Take QPDK, then rename again |
| `uv.lock` | QPDK | Take QPDK, then run `uv lock` |
### Add files, do not edit QPDK files
Each line you change in a QPDK file can conflict again on every later merge. Add new files instead:
- To change a cell, override it in your own module (see [Cell library](#cell-library)). Do not edit
`qpdk/cells/transmon.py`.
- To add cross-sections, define them in your own module and register them in `get_pdk()`, for example
`cross_sections={**get_cross_sections(tech), **get_cross_sections(my_tech)}`.
- To add tests, add new test files. Do not extend the QPDK ones.
(resolving-conflicts)=
### Resolving conflicts
Merge on a branch, so the result gets a review and CI like any other change:
```bash
git switch -c sync/upstream-v0.4.1 main
git merge upstream/v0.4.1
```
In a merge, `--ours` is your fork and `--theirs` is QPDK. Resolve conflicts by file type:
- **Identity and version files.** Resolve by hand. Keep your names and version, and take all other QPDK changes. Do not
use `git checkout --ours` on the whole file. Files such as `pyproject.toml` also contain QPDK dependency updates, and
you would lose them. Then check if QPDK added a new line with its name that you must rename.
- **`qpdk/tech.py` and `qpdk/layers.yaml`.** Resolve by hand. Keep your process values and take the QPDK additions. If
the layers changed, run `uv run python qpdk/tech.py` again.
- **Cells and models.** Merge as usual.
- **Regression references** (`tests/gds_ref/*.gds`, `tests/test_pdk/*.yml`, `.npz`). Do not merge them by hand. Keep
yours with `git checkout --ours -- ` and `git add `. After the merge, regenerate them with
`just test-gds-force`, and with `just test-models-force` if models changed. Commit them separately so the diff is easy
to review.
- **Other Git LFS files** (`tests/models/data/*.csv` and the `.parquet` datasets). Git merges the LFS pointer files as
text, so a conflict puts markers inside the pointer. Never edit the pointer. Pick one side and run `git add `.
Usually take QPDK with `git checkout --theirs -- `. Take `--ours` only if you regenerated the data. The `.npz`
files are also in LFS, but they are regression references. Always keep yours.
```{mermaid}
:alt: Conflicts are resolved by file type, as listed above, and every merge ends with just test and just run-pre.
flowchart LR
S["Conflicted
path"] --> I["Identity,
version"] --> A1["Keep your names,
take the rest"]
S --> P["tech.py,
layers.yaml"] --> A2["Merge by hand,
regen KLayout"]
S --> C["Cells,
models"] --> A3["Merge normally"]
S --> R["Regression
references"] --> A4["Keep ours,
regenerate"]
S --> L["Other Git
LFS files"] --> A5["Take one side,
never edit pointer"]
A1 & A2 & A3 & A4 & A5 --> T["just test,
just run-pre"]
```
Then run `just test` and `just run-pre`, and open a pull request for the merge. Merge it with a merge commit. Never
squash or rebase it, or the next merge conflicts on everything again.
### Taking a single change
To take one QPDK fix without a full merge, cherry-pick it. The `-x` flag records where it came from.
```bash
git cherry-pick -x
```
To reset one file to the QPDK version:
```bash
git checkout upstream/v0.4.1 -- qpdk/models/cpw.py
```
Neither of these causes a conflict at the next merge.
### Send generic fixes to QPDK
If you fix a model bug or write a cell that works in any process, open a pull request to QPDK. You then get it back with
the next merge and do not have to maintain it yourself. See the [contributing guide](contributing.md).
### Optional: automated sync pull requests
A scheduled CI job can prepare each merge for you. It pushes the latest QPDK tag to a `sync/upstream` branch and opens a
pull request from it. Below are examples for GitHub Actions, GitLab CI, and Forgejo Actions. The job does nothing if
`main` or `sync/upstream` already contains the tag.
If the pull request has conflicts, resolve them locally as in [Resolving conflicts](#resolving-conflicts). Then push the
result to the sync branch. The open pull request updates.
```bash
git push origin HEAD:sync/upstream
```
Merge that pull request before the next QPDK tag. The next run force-pushes the new tag to `sync/upstream` and replaces
your resolution. Merge the sync pull request with a merge commit. Never squash or rebase it.
#### GitHub Actions
Save this as `.github/workflows/sync-upstream.yml`:
```yaml
name: Sync upstream
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch:
permissions: {}
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.SYNC_TOKEN }}
- name: Push the latest upstream tag to sync/upstream and open a pull request
env:
GH_TOKEN: ${{ secrets.SYNC_TOKEN }}
run: |
git fetch --no-tags https://github.com/gdsfactory/quantum-rf-pdk.git '+refs/tags/v*:refs/tags/upstream/v*'
tag=$(git for-each-ref --sort=-v:refname --count=1 --format='%(refname:short)' 'refs/tags/upstream/v*')
if git merge-base --is-ancestor "$tag" origin/main; then
echo "Already up to date with $tag"
exit 0
fi
if git rev-parse -q --verify origin/sync/upstream >/dev/null &&
git merge-base --is-ancestor "$tag" origin/sync/upstream; then
echo "sync/upstream already has $tag"
exit 0
fi
git push --force origin "$tag^{commit}:refs/heads/sync/upstream"
if [ -z "$(gh pr list -R "$GITHUB_REPOSITORY" --head sync/upstream --json number --jq '.[].number')" ]; then
gh pr create -R "$GITHUB_REPOSITORY" --base main --head sync/upstream \
--title "Merge upstream ${tag#upstream/}" \
--body "Automated sync with QPDK ${tag#upstream/}. Regenerate the regression references before merging."
fi
```
Use a personal access token or GitHub App token as `SYNC_TOKEN`, not `GITHUB_TOKEN`. GitHub rejects `GITHUB_TOKEN`
pushes that change `.github/workflows/`, and QPDK releases often do. Also, pull requests opened with `GITHUB_TOKEN` do
not start your CI. A fine-grained token needs write access to contents, pull requests, and workflows. Pin the action to
a commit SHA, as QPDK does.
#### GitLab CI
Add this job to `.gitlab-ci.yml`. Then create a pipeline schedule for `main` in **Build > Pipeline schedules**.
```yaml
sync-upstream:
image:
name: alpine/git
entrypoint: [""]
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
variables:
GIT_DEPTH: 0
script:
- git fetch --no-tags https://github.com/gdsfactory/quantum-rf-pdk.git '+refs/tags/v*:refs/tags/upstream/v*'
- tag=$(git for-each-ref --sort=-v:refname --count=1 --format='%(refname:short)' 'refs/tags/upstream/v*')
- if git merge-base --is-ancestor "$tag" HEAD; then echo "Already up to date with $tag"; exit 0; fi
- remote="https://oauth2:${SYNC_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
- |
if git fetch -q "$remote" sync/upstream 2>/dev/null && git merge-base --is-ancestor "$tag" FETCH_HEAD; then
echo "sync/upstream already has $tag"
exit 0
fi
- >-
git push --force
-o merge_request.create -o merge_request.target=main
-o merge_request.title="Merge upstream ${tag#upstream/}"
"$remote" "$tag^{commit}:refs/heads/sync/upstream"
```
The `merge_request.*` push options open a merge request on the first push and update it later. Store a project access
token with the `write_repository` scope as the masked CI/CD variable `SYNC_TOKEN`. By default `CI_JOB_TOKEN` cannot
push.
#### Forgejo Actions
Save this as `.forgejo/workflows/sync-upstream.yml`. Set `runs-on` to a label of your runner. The image needs `git` and
`curl`.
```yaml
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch:
jobs:
sync:
runs-on: docker
steps:
- name: Push the latest upstream tag to sync/upstream and open a pull request
env:
SYNC_TOKEN: ${{ secrets.SYNC_TOKEN }}
SERVER: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: |
host=${SERVER#*://}
git clone -q "https://oauth2:${SYNC_TOKEN}@${host}/${REPO}.git" fork
cd fork
git fetch --no-tags https://github.com/gdsfactory/quantum-rf-pdk.git '+refs/tags/v*:refs/tags/upstream/v*'
tag=$(git for-each-ref --sort=-v:refname --count=1 --format='%(refname:short)' 'refs/tags/upstream/v*')
if git merge-base --is-ancestor "$tag" origin/main; then
echo "Already up to date with $tag"
exit 0
fi
if git rev-parse -q --verify origin/sync/upstream >/dev/null &&
git merge-base --is-ancestor "$tag" origin/sync/upstream; then
echo "sync/upstream already has $tag"
exit 0
fi
git push --force origin "$tag^{commit}:refs/heads/sync/upstream"
# 201: pull request opened. 409: one is already open for sync/upstream.
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$SERVER/api/v1/repos/$REPO/pulls" \
-H "Authorization: token $SYNC_TOKEN" -H 'Content-Type: application/json' \
-d "{\"base\":\"main\",\"head\":\"sync/upstream\",\"title\":\"Merge upstream ${tag#upstream/}\"}")
[ "$code" = 201 ] || [ "$code" = 409 ]
```
Store an access token with write access to the repository as the Actions secret `SYNC_TOKEN`. The automatic token may
not start CI on the pull request.
## Your first merge
After your identity and process commits:
1. Set up the `upstream` remote ([Setting up the fork](#setting-up-the-fork)).
1. Write down which paths you own, starting from the [ownership map](#ownership-map).
1. Find the latest `upstream/v*` tag and look at `git diff --stat main`.
1. Merge it on a branch and resolve conflicts by file type.
1. Run `just test-gds-force` and `just test`, commit the new references separately, and open a pull request.