Single transmon with readout resonator
In this example, we demonstrate the layout and solid model generation of a single transmon coupled to a readout resonator. In particular, this example demonstrates the additional steps necessary to generate a mesh suitable for electromagnetic simulation using external applications, such as the Palace solver. This example constructs the geometry used in the Palace release documentation and is aimed at demonstrating the power of the SolidModels
capability.
The full code for this example can be found in examples/SingleTransmon/SingleTransmon.jl
in the DeviceLayout.jl
repository. Components and process technology are drawn from the ExamplePDK.
Overview
The single transmon schematic design is simple, consisting of three components: an ExampleRectangleTransmon
, an ExampleClawedMeanderReadout
, and a coplanar waveguide path. The path additionally demonstrates how to terminate a path using lumped ports.
Once the mesh has been generated, configfile(sm::SolidModel; palace_build=nothing)
will ingest the SolidModel
and return a dictionary representing a Palace configuration. If the optional palace_build
directory is also passed then the configuration file will be validated using the Palace provided JSON schema.
The generated configuration and mesh files can then be run directly from within julia
using the palace_job(config::Dict; palace_build, np=0, nt=1)
which will write the config
to config.json
and if np > 0
attempt to run the Palace job from within the julia
shell mode. If np>0
the configuration file will be written to disk ready for a manual call to Palace outside of julia
.
These three functions are all wrapped together in main(palace_build, np=0)
which will build the SolidModel
, and write the mesh and configuration file to disk, before optionally attempting to run Palace.
SolidModel construction
To run the example we include
the example/SingleTransmon/SingleTransmon.jl
file and then call single_transmon()
. This will construct the schematic and render the design to SolidModel
. To visualize the resulting model in Gmsh
, we then call SolidModels.gmsh.fltk.run()
.
using DeviceLayout
include("examples/SingleTransmon/SingleTransmon.jl")
sm = SingleTransmon.single_transmon()
SolidModels.gmsh.fltk.run() # Opens Gmsh GUI
Below, we show the mesh for the metal surfaces. You can also (barely) see the lumped ports at the ends of the readout line in red.

The function exposes a number of parameters for the definition of the transmon and readout resonator, demonstrating how the kernel for an automated parameter search might be established. Aside from the design parameters there are two processing arguments: save_mesh::Bool
, which performs the meshing of the SolidModel
and writes the resulting mesh to disk as single_transmon.msh2
in the Gmsh 2.2 mesh format, and save_gds::Bool
, which generates a GDS of the schematic and writes the resulting design to single_transmon.gds
.
The schematic design is significantly simplied compared to that of the quantum processor example, but has the same general structure: the three components are added to the schematic graph and attached to each other before being placed by plan
into a floorplan, which is then furnished with air bridges.
What differs from the processor example is the construction of the SolidModel
. When we render!
the floorplan to a SolidModel
, instead of a LayoutTarget
we provide a SolidModelTarget
containing information about how to generate the 3D model. The SolidModelTarget
is based on this definition from ExamplePDK
:
"""
const SINGLECHIP_SOLIDMODEL_TARGET::SolidModelTarget
A `Target` for rendering to a `SolidModel` using the `ExamplePDK`'s process technology.
Contains rendering options and postrendering operations to create a solid model suitable
for simulation of a single-chip device (as opposed to a flipchip device).
"""
const SINGLECHIP_SOLIDMODEL_TARGET = SolidModelTarget(
EXAMPLE_SINGLECHIP_TECHNOLOGY; # Thickness and height define z-height and extrusions
simulation=true, # Optional simulation-only geometry entities will be rendered
bounding_layers=[:simulated_area], # SIMULATED_AREA defines the simulation bounds
substrate_layers=[:chip_area], # CHIP_AREA will be extruded downward
indexed_layers=[:port, :lumped_element, :integration], # Automatically index these layers
postrender_ops=[ # Manual definition of operations to run after 2D rendering
( # Get metal ground plane by subtracting negative from writeable area
"metal", # Output group name
SolidModels.difference_geom!, # Operation
("writeable_area", "metal_negative", 2, 2), # (object, tool, object_dim, tool_dim)
:remove_object => true # Remove "writeable_area" group after operation
),
( # Then add any positive back in
"metal",
SolidModels.union_geom!,
("metal", "metal_positive", 2, 2),
:remove_tool => true
),
( # Define a bulk physical group for all the substrates in the domain.
"substrate",
SolidModels.union_geom!,
("chip_area_extrusion", "chip_area_extrusion", 3, 3),
:remove_object => true,
:remove_tool => true
),
( # Define the vacuum domain as the remainder of the simulation domain.
"vacuum",
SolidModels.difference_geom!,
("simulated_area_extrusion", "substrate", 3, 3)
),
# Generate staple bridges in "bridge_metal" group
SolidModels.staple_bridge_postrendering(;
base="bridge_base",
bridge="bridge",
bridge_height=10μm # Exaggerated, for visualization
)...,
( # Union of all physical metal
"metal",
SolidModels.union_geom!,
("metal", "bridge_metal"),
:remove_object => true,
:remove_tool => true
),
(("metal", SolidModels.difference_geom!, ("metal", "port")))
],
# We only want to retain physical groups that we will need for specifying boundary
# conditions in the physical domain.
retained_physical_groups=[
("vacuum", 3),
("substrate", 3),
("metal", 2),
("exterior_boundary", 2)
]
)
We modify it slightly to retain port_1
, port_2
, and lumped_element
physical groups in order to use those in the simulation configuration.
Configuration
Next, we generate a dictionary defining a Palace configuration, specifying the problem type, model, materials, boundary conditions, and solver settings. We can also validate the configuration if we have a path to a Palace build, which contains the schema for validation. For more details on configuration, see the Palace documentation.
For this example, we define most of the configuration by hand. However, we need to identify which materials and boundary conditions apply to which volumes and surfaces in the model.
By construction, the SolidModel
contains physical groups identifying the volumes and surfaces we're interested in. Using SolidModels.attributes
, we can get a dictionary mapping the names of the physical groups to the integer "attribute" identifying the corresponding entities in the mesh. We can then use this dictionary to populate the configuration automatically according to the physical intent behind the model, without having to worry about whether the underlying geometry or attribute numbering might change.
"""
configfile(sm::SolidModel; palace_build=nothing)
Given a `SolidModel`, assemble a dictionary defining a configuration file for use within
Palace.
- `sm`: The `SolidModel`from which to construct the configuration file
- `palace_build = nothing`: Path to a Palace build directory, used to perform validation of
the configuration file. If not present, no validation is performed.
"""
function configfile(sm::SolidModel; palace_build=nothing)
attributes = SolidModels.attributes(sm)
config = Dict(
"Problem" => Dict(
"Type" => "Eigenmode",
"Verbose" => 2,
"Output" => joinpath(@__DIR__, "postpro/single-transmon")
),
"Model" => Dict(
"Mesh" => joinpath(@__DIR__, "single_transmon.msh2"),
"L0" => DeviceLayout.ustrip(m, 1SolidModels.STP_UNIT), # um is Palace default; record it anyway
"Refinement" => Dict(
"MaxIts" => 0 # Increase to enable AMR
)
),
"Domains" => Dict(
"Materials" => [
Dict(
# Vaccuum
"Attributes" => [attributes["vacuum"]],
"Permeability" => 1.0,
"Permittivity" => 1.0
),
Dict(
# Sapphire
"Attributes" => [attributes["substrate"]],
"Permeability" => [0.99999975, 0.99999975, 0.99999979],
"Permittivity" => [9.3, 9.3, 11.5],
"LossTan" => [3.0e-5, 3.0e-5, 8.6e-5],
"MaterialAxes" =>
[[0.8, 0.6, 0.0], [-0.6, 0.8, 0.0], [0.0, 0.0, 1.0]]
)
],
"Postprocessing" => Dict(
"Energy" => [Dict("Index" => 1, "Attributes" => [attributes["substrate"]])]
)
),
"Boundaries" => Dict(
"PEC" => Dict("Attributes" => [attributes["metal"]]),
"Absorbing" => Dict(
"Attributes" => [attributes["exterior_boundary"]],
"Order" => 1
),
"LumpedPort" => [
Dict(
"Index" => 1,
"Attributes" => [attributes["port_1"]],
"R" => 50,
"Direction" => "+X"
),
Dict(
"Index" => 2,
"Attributes" => [attributes["port_2"]],
"R" => 50,
"Direction" => "+X"
),
Dict(
"Index" => 3,
"Attributes" => [attributes["lumped_element"]],
"L" => 14.860e-9,
"C" => 5.5e-15,
"Direction" => "+Y"
)
]
),
"Solver" => Dict(
"Order" => 1,
"Eigenmode" => Dict("N" => 2, "Tol" => 1.0e-6, "Target" => 2, "Save" => 2),
"Linear" => Dict("Type" => "Default", "Tol" => 1.0e-7, "MaxIts" => 500)
)
)
if !isnothing(palace_build)
# Load the json schema and validate the configuration
schema_dir = joinpath(palace_build, "bin", "schema")
schema = Schema(
JSON.parsefile(joinpath(schema_dir, "config-schema.json"));
parent_dir=schema_dir
)
validate(schema, config)
end
return config
end
The output should look like this:
Configuration: 0.000082 seconds (327 allocations: 31.391 KiB)
Dict{String, Dict{String, Any}} with 5 entries:
"Problem" => Dict("Verbose"=>...)
"Boundaries" => Dict("LumpedPort"=>...)
"Model" => Dict("Refinement"=>...)
"Domains" => Dict("Postprocessing"=>...)
"Solver" => Dict("Eigenmode"=>...)
Palace
Finally, we write the configuration to a file, call Palace, and parse the computed eigenfrequencies from its output:
"""
palace_job(config::Dict; palace_build, np=0, nt=1)
Given a configuration dictionary, write and optionally run the Palace simulation.
Writes `config.json` to `@__DIR__` in order to pass the configuration into Palace.
- `config` - A configuration file defining the required fields for a Palace configuration file
- `palace_build` - Path to a Palace build.
- `np = 0` - Number of MPI processes to use in the call to Palace. If greater than 0 attempts
to call palace from within the Julia shell. Requires correct specification of `ENV[PATH]`.
- `nt = 1` - Number of OpenMp threads to use in the call to Palace (requires Palace built with
OpenMp)
"""
function palace_job(config::Dict; palace_build, np=0, nt=1)
# Write the configuration file to json, ready for Palace ingestion
println("Writing configuration file to $(joinpath(@__DIR__, "config.json"))")
open(joinpath(@__DIR__, "config.json"), "w") do f
return JSON.print(f, config)
end
if np > 0
# Call Palace using the generated configuration file.
# Record the terminal output and any error to files.
println("Running Palace: stdout sent to log.out, stderr sent to err.out")
withenv("PATH" => "$(ENV["PATH"]):$palace_build/bin") do
return run(
pipeline(
ignorestatus(
`palace -np $np -nt $nt $(joinpath(@__DIR__,"config.json"))`
),
stdout=joinpath(@__DIR__, "log.out"),
stderr=joinpath(@__DIR__, "err.out")
)
)
end
println("Complete.")
# Extract the computed frequencies
postprodir = joinpath(@__DIR__, config["Problem"]["Output"])
freq = CSV.File(joinpath(postprodir, "eig.csv"); header=1) |> DataFrame
println("Eigenmode Frequencies (GHz): ", freq[:, 2])
end
return nothing
end
The output should look like this:
Writing configuration file to /path/to/your/config.json
Running Palace: stdout sent to log.out, stderr sent to err.out
Complete.
Eigenmode Frequencies (GHz): [3.161773657, 4.878135762]
Palace: 52.914418 seconds (669 allocations: 46.422 KiB)