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:
Fork the repository and rename it in one commit.
Replace the process (
qpdk/layers.yamlandqpdk/tech.py) in one more commit.Remove or override cells, then regenerate the regression references.
Merge new QPDK release tags often. Use the 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
qpdkas a dependency. Build your owngdsfactory.Pdkin 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 tracks a project template for this.
flowchart LR
Q1["New layers, stack,<br/>or materials?"] -->|yes| F["Fork:<br/>own the process"]
Q1 -->|no| Q2["Only adding cells,<br/>samples, or chips?"]
Q2 -->|no| F
Q2 -->|yes| C["Compose:<br/>depend on qpdk"]
The rest of this page is about forks.
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.
git clone <your-fork-url> <your-pdk>
cd <your-pdk>
git remote add upstream https://github.com/gdsfactory/quantum-rf-pdk.git
# Your releases also create v<version> tags.
# Fetch the QPDK tags as upstream/v<version> 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#
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 |
|
Name and version |
|
Tooling paths |
|
GDSFactory+ project |
|
Layer map class |
|
PDK object |
|
KLayout technology |
|
KLayout install |
|
Docs |
|
Workflows |
|
Repo metadata |
Repository URLs in |
The justfile reads the package name from pdk := env('pdk', 'qpdk'). Set pdk=<your_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:
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, <symbols> 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 namesM1,Substrate, andVacuum. The CPW models andLAYER_STACK_NO_VACUUMlook 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#
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 inqpdk/cells/__init__.py. Give it a prefix, for exampleacme_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.pyimportsstraightdirectly, so it ignores yourstraight.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 sametags=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 ofqpdk/cells/__init__.pynotebooks/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/, andtests/models/test_models_regression/
When your process and cells are ready, regenerate the references. Check the diff before you commit.
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.
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, version |
Fork |
Keep your names. Take the other QPDK changes |
|
Fork |
Keep yours. Add QPDK layers you want, regenerate |
|
Both |
Keep your values. Take new QPDK definitions |
|
QPDK |
Take QPDK |
|
QPDK |
Take QPDK, unless you regenerated it |
QPDK modules in |
QPDK |
Merge. New QPDK cells usually merge cleanly |
Your cell modules, override modules, and test chips |
Fork |
QPDK never changes them |
|
Both |
Keep removed files removed. Take QPDK for others |
|
Fork |
Never merge. Keep yours, then regenerate |
Other tests, |
QPDK |
Take QPDK |
|
Both |
Keep your names. Take structure changes |
|
QPDK |
Take QPDK, then rename again |
|
QPDK |
Take QPDK, then run |
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). Do not edit
qpdk/cells/transmon.py.To add cross-sections, define them in your own module and register them in
get_pdk(), for examplecross_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#
Merge on a branch, so the result gets a review and CI like any other change:
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 --ourson the whole file. Files such aspyproject.tomlalso 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.pyandqpdk/layers.yaml. Resolve by hand. Keep your process values and take the QPDK additions. If the layers changed, runuv run python qpdk/tech.pyagain.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 withgit checkout --ours -- <path>andgit add <path>. After the merge, regenerate them withjust test-gds-force, and withjust test-models-forceif models changed. Commit them separately so the diff is easy to review.Other Git LFS files (
tests/models/data/*.csvand the.parquetdatasets). Git merges the LFS pointer files as text, so a conflict puts markers inside the pointer. Never edit the pointer. Pick one side and rungit add <path>. Usually take QPDK withgit checkout --theirs -- <path>. Take--oursonly if you regenerated the data. The.npzfiles are also in LFS, but they are regression references. Always keep yours.
flowchart LR
S["Conflicted<br/>path"] --> I["Identity,<br/>version"] --> A1["Keep your names,<br/>take the rest"]
S --> P["tech.py,<br/>layers.yaml"] --> A2["Merge by hand,<br/>regen KLayout"]
S --> C["Cells,<br/>models"] --> A3["Merge normally"]
S --> R["Regression<br/>references"] --> A4["Keep ours,<br/>regenerate"]
S --> L["Other Git<br/>LFS files"] --> A5["Take one side,<br/>never edit pointer"]
A1 & A2 & A3 & A4 & A5 --> T["just test,<br/>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.
git cherry-pick -x <commit>
To reset one file to the QPDK version:
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.
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. Then push the result to the sync branch. The open pull request updates.
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:
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.
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.
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:
Set up the
upstreamremote (Setting up the fork).Write down which paths you own, starting from the ownership map.
Find the latest
upstream/v*tag and look atgit diff --stat <tag> main.Merge it on a branch and resolve conflicts by file type.
Run
just test-gds-forceandjust test, commit the new references separately, and open a pull request.