Skip to content

Commit a832fb7

Browse files
authored
Improve single transmon example (#36)
* docs: Improve single transmon example * Address review comments and make small edits to single transmon example text
1 parent a2404f9 commit a832fb7

File tree

7 files changed

+494
-113
lines changed

7 files changed

+494
-113
lines changed

Diff for: docs/src/assets/single_transmon_mesh.png

-50.5 KB
Loading

Diff for: docs/src/examples/singletransmon.md

+259-27
Large diffs are not rendered by default.

Diff for: examples/SingleTransmon/Project.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[deps]
22
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
33
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
4+
DeviceLayout = "ebf59a4a-04ec-49d7-8cd4-c9382ceb8e85"
45
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
56
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
67
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
7-
DeviceLayout = "ebf59a4a-04ec-49d7-8cd4-c9382ceb8e85"
8+
PRIMA = "0a7d04aa-8ac2-47b3-b7a7-9dbd6ad661ed"

Diff for: examples/SingleTransmon/SingleTransmon.jl

+174-36
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import .SchematicDrivenLayout.ExamplePDK
88
import .SchematicDrivenLayout.ExamplePDK: LayerVocabulary, L1_TARGET, add_bridges!
99
using .ExamplePDK.Transmons, .ExamplePDK.ReadoutResonators
1010
import .ExamplePDK.SimpleJunctions: ExampleSimpleJunction
11+
import DeviceLayout: uconvert
12+
13+
using PRIMA
1114

1215
"""
1316
single_transmon(
@@ -34,34 +37,47 @@ function single_transmon(;
3437
cap_width=24μm,
3538
cap_length=620μm,
3639
cap_gap=30μm,
40+
total_length=5000μm,
3741
n_meander_turns=5,
3842
hanger_length=500μm,
3943
bend_radius=50μm,
4044
save_mesh::Bool=false,
41-
save_gds::Bool=false
45+
save_gds::Bool=false,
46+
mesh_order=2
4247
)
4348
#### Reset name counter for consistency within a Julia session
4449
reset_uniquename!()
45-
# Compute implicit parameters
46-
w_grasp = cap_width + 2 * cap_gap
47-
total_height = 444μm + hanger_length + (3 + n_meander_turns * 2) * bend_radius
4850

49-
#### Create abstract components
51+
#### Assemble schematic graph
52+
### Compute additional/implicit parameters
5053
cpw_width = 10μm
5154
cpw_gap = 6μm
5255
PATH_STYLE = Paths.SimpleCPW(cpw_width, cpw_gap)
5356
BRIDGE_STYLE = ExamplePDK.bridge_geometry(PATH_STYLE)
54-
57+
coupling_gap = 5μm
58+
w_grasp = cap_width + 2 * cap_gap
59+
arm_length = 428μm # straight length from meander exit to claw
60+
total_height =
61+
arm_length +
62+
coupling_gap +
63+
Paths.extent(PATH_STYLE) +
64+
hanger_length +
65+
(3 + n_meander_turns * 2) * bend_radius
66+
### Create abstract components
67+
## Transmon
5568
qubit = ExampleRectangleTransmon(;
5669
jj_template=ExampleSimpleJunction(),
5770
name="qubit",
5871
cap_length,
5972
cap_gap,
6073
cap_width
6174
)
75+
## Resonator
6276
rres = ExampleClawedMeanderReadout(;
6377
name="rres",
6478
coupling_length=400μm,
79+
coupling_gap,
80+
total_length,
6581
w_shield,
6682
w_claw,
6783
l_claw,
@@ -73,14 +89,6 @@ function single_transmon(;
7389
bend_radius,
7490
bridge=BRIDGE_STYLE
7591
)
76-
77-
##### Build schematic graph
78-
g = SchematicGraph("single-transmon")
79-
qubit_node = add_node!(g, qubit)
80-
rres_node = fuse!(g, qubit_node, rres)
81-
# Equivalent to `fuse!(g, qubit_node=>:readout, rres=>:qubit)`
82-
# because `matching_hooks` was implemented for that component pair
83-
8492
## Readout path
8593
readout_length = 2700μm
8694
p_readout = Path(
@@ -92,22 +100,31 @@ function single_transmon(;
92100
straight!(p_readout, readout_length / 2, PATH_STYLE)
93101
straight!(p_readout, readout_length / 2, PATH_STYLE)
94102

95-
## Readout lumped ports - one at each end
103+
# Readout lumped ports - squares on CPW trace, one at each end
96104
csport = CoordinateSystem(uniquename("port"), nm)
97105
render!(
98106
csport,
99107
only_simulated(centered(Rectangle(cpw_width, cpw_width))),
100108
LayerVocabulary.PORT
101109
)
110+
# Attach with port center `cpw_width` from the end (instead of `cpw_width/2`) to avoid corner effects
102111
attach!(p_readout, sref(csport), cpw_width, i=1) # @ start
103112
attach!(p_readout, sref(csport), readout_length / 2 - cpw_width, i=2) # @ end
113+
114+
#### Build schematic graph
115+
g = SchematicGraph("single-transmon")
116+
qubit_node = add_node!(g, qubit)
117+
rres_node = fuse!(g, qubit_node, rres)
118+
# Equivalent to `fuse!(g, qubit_node=>:readout, rres=>:qubit)`
119+
# because `matching_hooks` was implemented for that component pair
104120
p_readout_node = add_node!(g, p_readout)
121+
105122
## Attach resonator to feedline
106123
# Instead of `fuse!` we use a schematic-based `attach!` method to place it along the path
107124
# Syntax is a mix of `fuse!` and how we attached the ports above
108125
attach!(g, p_readout_node, rres_node => :feedline, 0mm, location=1)
109126

110-
## Position the components.
127+
#### Create the schematic (position the components)
111128
floorplan = plan(g)
112129
add_bridges!(floorplan, BRIDGE_STYLE, spacing=300μm) # Add bridges to paths
113130

@@ -120,8 +137,11 @@ function single_transmon(;
120137
chip = centered(Rectangle(substrate_x, substrate_y), on_pt=center_xyz)
121138
sim_area = centered(Rectangle(substrate_x, substrate_y), on_pt=center_xyz)
122139

140+
# Define bounds for bounding simulation box
123141
render!(floorplan.coordinate_system, sim_area, LayerVocabulary.SIMULATED_AREA)
142+
# postrendering operations in solidmodel target define metal = (WRITEABLE_AREA - METAL_NEGATIVE) + METAL_POSITIVE
124143
render!(floorplan.coordinate_system, sim_area, LayerVocabulary.WRITEABLE_AREA)
144+
# Define rectangle that gets extruded to generate substrate volume
125145
render!(floorplan.coordinate_system, chip, LayerVocabulary.CHIP_AREA)
126146

127147
check!(floorplan)
@@ -134,37 +154,43 @@ function single_transmon(;
134154
# resolution near edges of the geometry.
135155
meshing_parameters = SolidModels.MeshingParameters(
136156
mesh_scale=1.0,
137-
mesh_order=2,
157+
α_default=0.9,
158+
mesh_order=mesh_order,
138159
options=Dict("General.Verbosity" => 1) # General Gmsh option input
139160
)
140161
render!(sm, floorplan, tech, meshing_parameters=meshing_parameters)
141162

142163
if save_mesh
164+
# SolidModels.gmsh.option.set_number("General.NumThreads", 1) # Force single-threaded (deterministic) meshing
143165
SolidModels.gmsh.model.mesh.generate(3) # runs without error
144166
save(joinpath(@__DIR__, "single_transmon.msh2"), sm)
145167
end
146168

147169
if save_gds
148-
# Render to GDS as well.
170+
# Render to GDS as well, may be useful to debug SolidModel generation
149171
c = Cell("single_transmon", nm)
150-
render!(c, floorplan, L1_TARGET, strict=:no)
172+
# Use simulation=true to render simulation-only geometry, `strict=:no` to continue from errors
173+
render!(c, floorplan, L1_TARGET, strict=:no, simulation=true)
151174
flatten!(c)
152175
save(joinpath(@__DIR__, "single_transmon.gds"), c)
153176
end
154177
return sm
155178
end
156179

157180
"""
158-
configfile(sm::SolidModel; palace_build=nothing)
181+
configfile(sm::SolidModel; palace_build=nothing, solver_order=2, amr=0)
159182
160183
Given a `SolidModel`, assemble a dictionary defining a configuration file for use within
161184
Palace.
162185
163186
- `sm`: The `SolidModel`from which to construct the configuration file
164187
- `palace_build = nothing`: Path to a Palace build directory, used to perform validation of
165188
the configuration file. If not present, no validation is performed.
189+
- `solver_order = 2`: Finite element order (degree) for the solver. Palace supports arbitrary
190+
high-order spaces.
191+
- `amr = 0`: Maximum number of adaptive mesh refinement (AMR) iterations.
166192
"""
167-
function configfile(sm::SolidModel; palace_build=nothing)
193+
function configfile(sm::SolidModel; palace_build=nothing, solver_order=2, amr=0)
168194
attributes = SolidModels.attributes(sm)
169195

170196
config = Dict(
@@ -175,9 +201,9 @@ function configfile(sm::SolidModel; palace_build=nothing)
175201
),
176202
"Model" => Dict(
177203
"Mesh" => joinpath(@__DIR__, "single_transmon.msh2"),
178-
"L0" => DeviceLayout.ustrip(m, 1SolidModels.STP_UNIT), # um is Palace default; record it anyway
204+
"L0" => 1e-6, # um is Palace default; record it anyway
179205
"Refinement" => Dict(
180-
"MaxIts" => 0 # Increase to enable AMR
206+
"MaxIts" => amr # Nonzero to enable AMR
181207
)
182208
),
183209
"Domains" => Dict(
@@ -231,8 +257,8 @@ function configfile(sm::SolidModel; palace_build=nothing)
231257
]
232258
),
233259
"Solver" => Dict(
234-
"Order" => 1,
235-
"Eigenmode" => Dict("N" => 2, "Tol" => 1.0e-6, "Target" => 2, "Save" => 2),
260+
"Order" => solver_order,
261+
"Eigenmode" => Dict("N" => 2, "Tol" => 1.0e-6, "Target" => 1, "Save" => 2),
236262
"Linear" => Dict("Type" => "Default", "Tol" => 1.0e-7, "MaxIts" => 500)
237263
)
238264
)
@@ -257,12 +283,12 @@ Given a configuration dictionary, write and optionally run the Palace simulation
257283
258284
Writes `config.json` to `@__DIR__` in order to pass the configuration into Palace.
259285
260-
- `config` - A configuration file defining the required fields for a Palace configuration file
261-
- `palace_build` - Path to a Palace build.
262-
- `np = 0` - Number of MPI processes to use in the call to Palace. If greater than 0 attempts
286+
- `config`: A configuration file defining the required fields for a Palace configuration file
287+
- `palace_build`: Path to a Palace build.
288+
- `np = 0`: Number of MPI processes to use in the call to Palace. If greater than 0 attempts
263289
to call palace from within the Julia shell. Requires correct specification of `ENV[PATH]`.
264-
- `nt = 1` - Number of OpenMp threads to use in the call to Palace (requires Palace built with
265-
OpenMp)
290+
- `nt = 1`: Number of OpenMP threads to use in the call to Palace (requires Palace built with
291+
OpenMP)
266292
"""
267293
function palace_job(config::Dict; palace_build, np=0, nt=1)
268294
# Write the configuration file to json, ready for Palace ingestion
@@ -293,24 +319,136 @@ function palace_job(config::Dict; palace_build, np=0, nt=1)
293319
freq = CSV.File(joinpath(postprodir, "eig.csv"); header=1) |> DataFrame
294320

295321
println("Eigenmode Frequencies (GHz): ", freq[:, 2])
322+
return freq[:, 2]
296323
end
297324
return nothing
298325
end
299326

300327
"""
301-
main(palace_build, np=0)
328+
compute_eigenfrequencies(palace_build, np=1; solver_order=2, mesh_order=2, cap_length=620μm, total_length=5000μm))
302329
303330
Given a build of Palace found at `palace_build`, assemble a `SolidModel` of the single
304331
transmon example, mesh the geometry, define a configuration file for the geometry, and if
305-
`np > 0`, launch Palace from the provided `palace_build`.`
332+
`np > 0`, launch Palace from the provided `palace_build`. (If `0`, use `palace_build`
333+
only to validate config.)
334+
335+
# Keyword arguments
336+
337+
- `solver_order = 2`: Finite element order (degree) for the solver. Palace supports arbitrary
338+
high-order spaces.
339+
- `mesh_order = 2`: Polynomial order used to represent the element geometries in the mesh.
340+
- `cap_length = 620μm`: Length of transmon island capacitor.
341+
- `total_length = 5000μm`: Total length of readout resonator.
306342
"""
307-
function main(palace_build, np=0)
343+
function compute_eigenfrequencies(
344+
palace_build,
345+
np=1;
346+
solver_order=2,
347+
mesh_order=2,
348+
cap_length=620μm,
349+
total_length=5000μm
350+
)
308351
# Construct the SolidModel
309-
@time "SolidModel + Meshing" sm = single_transmon(save_mesh=true)
352+
@time "SolidModel + Meshing" sm = single_transmon(
353+
save_mesh=true;
354+
cap_length=cap_length,
355+
total_length=total_length,
356+
mesh_order=mesh_order
357+
)
310358
# Assemble the configuration
311-
@time "Configuration" config = configfile(sm; palace_build)
359+
@time "Configuration" config = configfile(sm; palace_build, solver_order=solver_order)
312360
# Call Palace
313-
@time "Palace" palace_job(config; palace_build, np)
361+
@time "Palace" freqs = palace_job(config; palace_build, np)
362+
return freqs
363+
end
364+
365+
"""
366+
frequency_targeting_errfunc(targets_GHz, palace_build, np, solver_order, mesh_order, freq_log, param_log)
367+
368+
Create an error function that can be minimized by an optimization routine to reach target frequencies.
369+
370+
The returned function takes parameters that control the transmon's capacitor length and resonator's total length.
371+
It runs `compute_eigenfrequencies` and returns the mean squared relative error between the computed
372+
eigenfrequencies and `targets_GHz`.
373+
It also pushes frequencies and parameter values to the provided arrays `freq_log` and `param_log`.
374+
"""
375+
function frequency_targeting_errfunc(
376+
targets_GHz,
377+
palace_build,
378+
np,
379+
solver_order,
380+
mesh_order,
381+
freq_log,
382+
param_log
383+
)
384+
return function errfunc(x, p=()) # Many optimizer interfaces require a second argument for fixed parameters
385+
freqs = compute_eigenfrequencies(
386+
palace_build,
387+
np;
388+
cap_length=(1 / x[1]^2) * 620μm, # Transform so that frequency(x) is approximately linear
389+
total_length=(1 / x[2]) * 5000μm, # ... and x_i are all ~1
390+
solver_order,
391+
mesh_order
392+
)[1:2] # [1:2] because technically Palace can find more than two eigenfrequencies
393+
push!(freq_log, freqs) # Log for later convenience
394+
push!(param_log, copy(x)) # Copy because `x` gets reused
395+
return sum((freqs .- targets_GHz) .^ 2 ./ targets_GHz .^ 2) / 2 # Mean squared relative error
396+
end
397+
end
398+
399+
"""
400+
run_optimization(palace_build, np=1; targets_GHz=[3.0, 4.0], reltol=1e-2, solver_order=2, mesh_order=2)
401+
402+
Optimize the transmon and resonator parameters to achieve target frequencies.
403+
404+
The objective function generates a new schematic, SolidModel, and mesh;
405+
generates and validates a new Palace configuration file; runs Palace using the mesh
406+
and configuration file; and returns the mean squared relative error between the
407+
computed eigenfrequencies and `targets_GHz`.
408+
The optimization routine stops when the mean squared relative error is less than `reltol^2`.
409+
410+
`np`, `solver_order`, and `mesh_order` are used for configuring and running Palace as in
411+
`compute_eigenfrequencies`.
412+
"""
413+
function run_optimization(
414+
palace_build,
415+
np=1;
416+
targets_GHz=[3.0, 4.0],
417+
reltol=1e-2,
418+
solver_order=2,
419+
mesh_order=2
420+
)
421+
freq_log = []
422+
param_log = []
423+
errfunc = frequency_targeting_errfunc( # Create the objective function for optimization
424+
targets_GHz,
425+
palace_build,
426+
np,
427+
solver_order,
428+
mesh_order,
429+
freq_log,
430+
param_log
431+
)
432+
final_params, info = prima( # Run the optimization
433+
errfunc,
434+
[1.0, 1.0]; # Initial parameters
435+
ftarget=reltol^2, # Stop when `errfunc(x) < reltol^2`
436+
xl=[0.6, 0.6], # Lower bounds
437+
xu=[1.4, 1.4], # Upper bounds
438+
rhobeg=0.2 # Initial trust region radius
439+
)
440+
println("""
441+
Number of Palace runs: $(info.nf)
442+
Initial parameters:
443+
Transmon capacitor_length = 620.0μm
444+
Resonator total_length = 5000.0μm
445+
Initial frequencies: $(round.(first(freq_log), digits=3)) GHz
446+
Final parameters:
447+
Transmon capacitor_length = $(round(μm, 620.0μm/final_params[1]^2, digits=3))
448+
Resonator total_length = $(round(μm, 5000.0μm/final_params[2], digits=3))
449+
Final frequencies: $(round.(last(freq_log), digits=3)) GHz
450+
""")
451+
return final_params, info, freq_log, param_log
314452
end
315453

316454
end # module

0 commit comments

Comments
 (0)