Skip to content

Methods for generating PDK and component packages #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.version == 'pre' }}
timeout-minutes: 60
permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created
actions: write
Expand All @@ -36,6 +37,7 @@ jobs:
matrix:
version:
- '1.10'
- '1.11'
- 'pre'
os:
- ubuntu-latest
Expand All @@ -44,6 +46,9 @@ jobs:
needs: [julia-format]
steps:
- uses: actions/checkout@v4
- run: |
git config --global user.name github-actions
git config --global user.email github-actions@github.com
- uses: actions/setup-python@v5
with:
python-version: '3.13'
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format of this changelog is based on

## Upcoming

- Added `generate_pdk`, `generate_component_package`, and `generate_component_definition` to `SchematicDrivenLayout` to help users create packages and files from templates
- Lowered default for meshing parameter `α_default` from `1.0` to `0.9` to improve robustness
- Docs: Added closed-loop optimization example with single transmon
- Docs: Updated to clarify that `build!` is not necessary

### Fixed

- `launch!` without rounding now has the correct gap behind the pad
Expand Down
4 changes: 4 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Memoize = "c03570c3-d221-55d1-a50c-7939bbd78826"
MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5"
NamedTupleTools = "d9ec5142-1e00-5aa0-9d6a-321866360f50"
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PkgTemplates = "14b8a8f1-9102-5b29-a752-f990bacb7fe1"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Expand Down Expand Up @@ -59,6 +61,8 @@ Memoize = "0.4"
MetaGraphs = "0.7"
NamedTupleTools = "0.14"
Optim = "1"
Pkg = "1"
PkgTemplates = "0.7"
PrecompileTools = "1"
Preferences = "1"
QuadGK = "2"
Expand Down
10 changes: 10 additions & 0 deletions docs/src/schematicdriven/pdks.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ Here, component packages have their own `Project.toml` file distinct from MyPDKP
with all Julia packages it is not advised to actually commit `Manifest.toml` files, although it is useful to commit them to one-off "projects" like analyses and layout scripts to enable fully reproducible environments.

This structure can be combined with [LocalRegistry](https://github.com/GunnarFarneback/LocalRegistry.jl/) to make the PDK and component packages available from a private registry. Doing so allows you to use the full power of the [Julia package manager](https://pkgdocs.julialang.org/) by versioning physical designs using semantic versioning, seamlessly tracking and switching between versions as needed.

## PDK tools

DeviceLayout.jl provides some utilities for generating packages and files for PDKs and components from templates:

```@docs
SchematicDrivenLayout.generate_component_definition
SchematicDrivenLayout.generate_component_package
SchematicDrivenLayout.generate_pdk
```
2 changes: 1 addition & 1 deletion scripts/format.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Usage: julia format.jl <action>
using Pkg
Pkg.add("JuliaFormatter")
Pkg.add(name="JuliaFormatter", version="1")
using JuliaFormatter
# Directories to format (recursive); paths relative to repo root
dirs = ["src", "test", "scripts"]
Expand Down
4 changes: 4 additions & 0 deletions src/schematics/SchematicDrivenLayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export @component,
flatten,
flatten!,
fuse!,
generate_component_definition,
generate_component_package,
generate_pdk,
_geometry!,
geometry,
graph,
Expand Down Expand Up @@ -132,6 +135,7 @@ include("components/builtin_components.jl")
include("routes.jl")
include("components/variants.jl")
include("solidmodels.jl")
include("pdktools.jl")

include("ExamplePDK/ExamplePDK.jl")

Expand Down
299 changes: 299 additions & 0 deletions src/schematics/pdktools.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
using Pkg, UUIDs
using PkgTemplates

"""
generate_pdk(name="MyPDK"; dir=pwd(), template=get_template("PDK.jlt"), kwargs...)

Generates a PDK package named `name` in the parent directory `dir` based on `template`.

Additional keyword arguments are forwarded to [`PkgTemplates.Template`](https://juliaci.github.io/PkgTemplates.jl/stable/user/#PkgTemplates.Template).

The PDK package can be registered in your private registry `MyRegistry` as follows
using the `LocalRegistry` package. First, make sure you are on a branch of the
`MyRegistry` registry in `~/.julia/registries/MyRegistry`. Then add the `LocalRegistry`
package to your active environment and run:

```julia
using LocalRegistry
register(
"MyPDK";
registry="MyRegistry",
push=false,
repo="git@ssh.example.com:path/to/MyPDK.jl.git" # or however you usually get your repo
)
```

You will need to push the changes and make a pull request for your branch.

For more information about creating and using a local registry,
see [the LocalRegistry README](https://github.com/GunnarFarneback/LocalRegistry.jl?tab=readme-ov-file#localregistry).
"""
function generate_pdk(name="MyPDK"; dir=pwd(), template=get_template("PDK.jlt"), kwargs...)
# Create package template
t = Template(;
dir=dir,
plugins=[
!License,
!CompatHelper,
!TagBot,
!GitHubActions,
!Dependabot,
SrcDir(; file=template),
Documenter{NoDeploy}(),
Git(
ignore=[
"components/*/Manifest.toml",
"components/*/docs/Manifest.toml",
"components/*/docs/build/"
]
)
],
kwargs...
)

# Generate package from template, but don't automatically precompile (no deps yet)
without_precompile() do
return t(name)
end

# Upper-bound the PDK package by major version and add deps.
update_package_toml!(joinpath(dir, name), ["DeviceLayout"])

# Create components directory
mkdir(joinpath(dir, name, "components"))

return
end

"""
get_template(template; pdk=nothing)

Get the full path to the most appropriate template with the filename `template`.

If `pdk` is not `nothing`, and the file named `template` exists in the `templates`
folder at the PDK package root, then that template will be used. Otherwise, the
built-in DeviceLayout.jl template will be used.
"""
function get_template(template; pdk=nothing)
if !isnothing(pdk) # If the PDK has its own template, use that
template_path = joinpath(pkgdir(pdk), "templates", template)
isfile(template_path) && return template_path
end # Otherwise, use the built-in template
return joinpath(pkgdir(@__MODULE__), "templates", template)
end

function without_precompile(f)
pc = get(ENV, "JULIA_PKG_PRECOMPILE_AUTO", nothing) # Original setting
ENV["JULIA_PKG_PRECOMPILE_AUTO"] = 0 # Zero => don't precompile, any other setting => do
try
f()
finally
if isnothing(pc)
delete!(ENV, "JULIA_PKG_PRECOMPILE_AUTO") # Restore default behavior
else
ENV["JULIA_PKG_PRECOMPILE_AUTO"] = pc # Restore original setting
end
end
end

function update_package_toml!(path, add_pkgs, dev_paths=[]; set_unit_pref=true, compat=true)
compat_dict = Dict{String, String}()
PkgTemplates.with_project(path) do
# Use Pkg to make sure manifest is immediately usable without needing resolve or dev
without_precompile() do
Pkg.add(add_pkgs)
for path in dev_paths
Pkg.develop(path=path)
end
end
if compat # Add versions to dict, we'll write to TOML later ourselves
for (name, uuid) in pairs(Pkg.project().dependencies)
v = Pkg.dependencies()[uuid].version # Could be v"x.y.z-DEV" etc
compat_dict[name] = join([v.major, v.minor, v.patch], ".") # Just "x.y.z"
end
end
end
!(set_unit_pref || compat) && return
# Write remaining project info manually
pkgtoml = Pkg.TOML.parsefile(joinpath(path, "Project.toml"))
if set_unit_pref # Set unit preference to current environment's value
# Edit the TOML directly, no need to precompile anything
pkgtoml["preferences"] = merge(
get(pkgtoml, "preferences", Dict()),
Dict("DeviceLayout" => Dict("units" => DeviceLayout.unit_preference))
)
end
if compat
# Usually `add` automatically adds compat if active env is a package
# But we have to do it manually for some reason
# We'll write it to Project.toml directly rather than use Pkg.compat
# Because we know currently used versions are valid and can avoid slow checks
pkgtoml["compat"] = merge(get(pkgtoml, "compat", Dict()), compat_dict)
end
open(joinpath(path, "Project.toml"), "w") do io # Write back to file
return Pkg.TOML.print(io, pkgtoml)
end
end

"""
generate_component_package(name::AbstractString, pdk::Module, compname="MyComp";
composite=false,
template=get_template(composite ? "CompositeComponent.jlt" : "Component.jlt", pdk=pdk)
docs_template=get_template("Component.mdt", pdk=pdk),
kwargs...
)

Generates a new component package named `name` in the components directory of `pdk`.

Adds `pdk` and `DeviceLayout` as dependencies and sets non-inclusive upper bounds of the
next major versions.
Creates a definition for a `Component` type named `compname` in the main module file, using
a template for standard components or for composite components depending on the keyword
argument `composite`.
If the `template` keyword is not
explicitly used, then if the PDK defines a `Component.jlt` or `CompositeComponent.jlt`
template in a `templates` folder at the package root, that will be used; otherwise,
the built-in DeviceLayout templates are used.
The source file generated in this way should not be `include`d from the PDK source files,
since it is an independent package even if it is tracked in the same Git repository.

Also generates documentation based on `docs_template`.

The component package can be registered in your private registry `MyRegistry`
as follows using the `LocalRegistry` package. First,
make sure you are on a branch of the `MyRegistry` registry in
`~/.julia/registries/MyRegistry`. Then add the `LocalRegistry` package to your active
environment and run:

```julia
using LocalRegistry
register(
name;
registry="MyRegistry",
push=false,
repo="git@ssh.example.com:path/to/MyPDK.jl.git" # or however you usually get your repo
)
```

You will need to push the changes and make a pull request for your branch.

For more information about creating and using a local registry,
see [the LocalRegistry README](https://github.com/GunnarFarneback/LocalRegistry.jl?tab=readme-ov-file#localregistry).
"""
function generate_component_package(
name::AbstractString,
pdk::Module,
compname="MyComp";
composite=false,
template=get_template(composite ? "CompositeComponent.jlt" : "Component.jlt", pdk=pdk),
docs_template=get_template("Component.mdt", pdk=pdk),
kwargs...
)
# Is PDK dev'd or not?
if !(Pkg.project().name == string(pdk)) # (or the active project, that works too)
pdk_pkginfo = Pkg.dependencies()[Pkg.project().dependencies[string(pdk)]]
pdk_pkginfo.is_tracking_path || error(
"$pdk must be the active project or you must run `using Pkg; Pkg.develop(\"$pdk\")` before generating a component package."
)
end

# Create package template
t = Template(;
dir=pdk.COMPONENTS_DIR,
plugins=[
!Git,
!License,
!CompatHelper,
!TagBot,
!GitHubActions,
!Dependabot,
SrcDir(; file=template),
Documenter{NoDeploy}(index_md=docs_template, devbranch="broken") # https://github.com/JuliaCI/PkgTemplates.jl/issues/463
],
kwargs...
)

# Generate package from template, but don't automatically precompile (no deps yet)
without_precompile() do
return t(name)
end

# Make src/docs template replacements manually
# (because we can't dynamically redefine PkgTemplates.user_view methods)
# (well, we could, but this way avoids both eval tricks and piracy)
write_from_template(
joinpath(pdk.COMPONENTS_DIR, name, "src", name * ".jl"),
template,
string(pdk),
name,
compname
)
write_from_template(
joinpath(pdk.COMPONENTS_DIR, name, "docs", "src", "index.md"),
docs_template,
string(pdk),
name,
compname
)

# upper-bound the PDK package by major version and add deps.
update_package_toml!(
joinpath(pdk.COMPONENTS_DIR, name),
["DeviceLayout"],
[pkgdir(pdk)] # dev pdk
)
# Same thing for docs
update_package_toml!(
joinpath(pdk.COMPONENTS_DIR, name, "docs"),
["DeviceLayout", "FileIO"], # add FileIO for convenience
[pkgdir(pdk)], # dev pdk (component package is already dev'd)
compat=false
)

return nothing
end

function write_from_template(filepath, template, pdkname, pkgname, compname)
open(template, "r") do io
template = read(io, String)
str = replace(
template,
"{{{pdkname}}}" => pdkname,
"{{{PKG}}}" => pkgname,
"{{{compname}}}" => compname
)
open(filepath, "w") do io
return write(io, str)
end
end
end

"""
generate_component_definition(compname, pdk::Module, filepath; composite=false,
template=get_template(composite ? "CompositeComponent.jlt" : "Component.jlt", pdk=pdk))

Generates a file defining the component type `compname` at `filepath` based on `template`.

Uses a template at the file path `template` for standard components or for composite components
depending on the keyword argument `composite`. If the `template` keyword is not
explicitly used, then if the PDK defines a `Component.jlt` or `CompositeComponent.jlt`
template in a `templates` folder at the package root, that will be used; otherwise,
the built-in DeviceLayout templates are used.

For generating a new component package, see `generate_component_package`.
Closely related components that should always be versioned together can be defined in
the same package, in which case this method can be used to generate only the file defining
a component. That file can then be `include`d from the file defining the root package module.

The built-in template defines a module because it's also used for package generation,
but it is not necessary for every component in a package to be in its own module.
"""
function generate_component_definition(
compname::AbstractString,
pdk::Module,
filepath;
composite=false,
template=get_template(composite ? "CompositeComponent.jlt" : "Component.jlt", pdk=pdk)
)
return write_from_template(filepath, template, string(pdk), compname * "s", compname)
end
Loading
Loading