Data-Driven Design with ParameterSet
In Building a Component and Composite Components, component parameters were written directly into Julia code. For larger designs — or designs that need to vary between fabrication runs, simulation sweeps, or process nodes — it's useful to move parameters out of the source and into external configuration. ParameterSet is a mutable parameter source that holds a nested dictionary (typically loaded from YAML) and feeds values into your components by address.
In this tutorial, you'll drive a simple component, then a composite transmon, from a ParameterSet instead of hardcoded parameters.
What You'll Learn
- Creating a
ParameterSetprogrammatically or from YAML - Reading values with dot syntax or
resolve - Instantiating components from a
ParameterSet - Attaching a
ParameterSetto aSchematicGraph - Forwarding shared parameters into composite subcomponents
- Auditing which parameters were actually consumed
Prerequisites
- Completed Building a Component tutorial
- Completed Composite Components tutorial
Setup
using DeviceLayout, .PreferredUnits
using DeviceLayout.SchematicDrivenLayoutEvery ParameterSet contains two required top-level namespaces:
global— parameters shared across the design (e.g., version, process node)components— per-component parameter trees
Creating a ParameterSet
The simplest way is to build one programmatically:
ps = ParameterSet()
# Set global metadata
ps.global.version = 1
ps.global.process_node = "fab_v3"
# Define component parameters with units
ps.components.capacitor.finger_length = 150μm
ps.components.capacitor.finger_width = 5μm
ps.components.capacitor.finger_gap = 3μm
ps.components.capacitor.finger_count = 6
ps.components.junction.w_jj = 1μm
ps.components.junction.h_jj = 1μmIf you have the YAML package installed, you can load directly from a file:
# design_params.yaml
global:
version: 1
process_node: fab_v3
components:
capacitor:
finger_length: 150μm
finger_width: 5μm
finger_gap: 3μm
finger_count: 6
junction:
w_jj: 1μm
h_jj: 1μmusing YAML # activates the ParameterSetYAMLExt extension
ps = ParameterSet("design_params.yaml")Reading Parameters
Use dot syntax to navigate the hierarchy:
ps.global.version # => 1
ps.components.capacitor # => ParameterSet scoped to capacitor subtree
ps.components.capacitor.finger_length # => 150μmOr use resolve with a dot-separated address:
resolve(ps, "components.capacitor.finger_length") # => 150μm
resolve(ps, "components.capacitor") # => scoped ParameterSetExtract all leaf parameters at a level as a NamedTuple:
leaf_params(ps.components.capacitor)
# => (finger_length = 150μm, finger_width = 5μm, finger_gap = 3μm, finger_count = 6)Simple Components with ParameterSet
Suppose you have a component defined with @compdef:
@compdef struct MyCapacitor <: Component
name = "capacitor"
finger_length = 100μm
finger_width = 5μm
finger_gap = 3μm
finger_count::Int = 4
endYou can instantiate it from the ParameterSet using create_component:
cap = create_component(MyCapacitor, ps, "components.capacitor")This resolves "components.capacitor" in the parameter set, extracts leaf parameters, and passes them as keyword arguments to the MyCapacitor constructor. Parameters not present in the ParameterSet keep their defaults.
Consumed parameters are tracked in ps.accessed, which is useful for auditing which parameters were actually used:
ps.accessed
# => Set(["components.capacitor.finger_length", "components.capacitor.finger_width", ...])Attaching ParameterSet to a SchematicGraph
Pass the ParameterSet when creating a SchematicGraph so that all components in the graph can access it:
g = SchematicGraph("my_design", ps)
# The parameter set is accessible from the graph
g.parameter_set.components.capacitor.finger_length # => 150μmA full example with simple components:
# Load parameters
ps = ParameterSet()
ps.components.cap1.finger_length = 150μm
ps.components.cap1.finger_count = 6
ps.components.cap2.finger_length = 200μm
ps.components.cap2.finger_count = 8
# Create graph with parameter set
g = SchematicGraph("two_caps", ps)
# Create components from parameter set
cap1 = create_component(MyCapacitor, ps, "components.cap1")
cap2 = create_component(MyCapacitor, ps, "components.cap2")
# Build schematic
cap1_node = add_node!(g, cap1)
cap2_node = fuse!(g, cap1_node => :p1, cap2 => :p0)
sch = plan(g; log_dir=nothing)Composite Components with ParameterSet
For composite components, the ParameterSet propagates through the graph hierarchy. When you attach a ParameterSet to a top-level SchematicGraph, it is available inside _build_subcomponents via the graph.
With a ParameterSet, subcomponent parameters live in the parameter set rather than in the composite struct. The composite only declares parameters that are shared across multiple subcomponents:
@compdef struct SimpleTransmon <: CompositeComponent
name = "transmon"
junction_gap = 12μm # shared: controls both island gap and junction height
endDefine the parameter set with a namespace per subcomponent. Note that junction_gap only appears on the composite — it will be forwarded to subcomponents in _build_subcomponents:
ps = ParameterSet()
ps.components.transmon.junction_gap = 12μm
ps.components.transmon.island.cap_width = 24μm
ps.components.transmon.island.cap_length = 520μm
ps.components.transmon.island.cap_gap = 30μm
ps.components.transmon.junction.w_jj = 1μm
ps.components.transmon.junction.h_jj = 1μmInside _build_subcomponents, use parameter_set(g) to access the graph's ParameterSet, then create_component to instantiate each subcomponent from its subtree. The shared junction_gap is read from the parameter set and forwarded to both subcomponents under their respective parameter names using set_parameters kwargs:
function SchematicDrivenLayout._build_subcomponents(tr::SimpleTransmon)
ps = parameter_set(tr._graph)
island = create_component(ExampleRectangleIsland, ps, "components.transmon.island")
# Forward shared parameter from under the parent component
# No need to count its access here because it has been accessed
# during the CC construction
island = set_parameters(island; junction_gap=tr.junction_gap)
junction = create_component(ExampleSimpleJunction, ps, "components.transmon.junction")
# Forward shared parameter from parameter set to the junction component
# under a different parameter name. A missing PS lookup would surface as
# ParameterKeyError at this call site.
junction = set_parameters(junction; h_ground_island=ps.components.transmon.junction_gap)
return (island, junction)
endcreate_component(T, ps, address) resolves the address, extracts leaf parameters via leaf_params, and passes them as keyword arguments to the component constructor. Parameters not in the ParameterSet keep their defaults.
Create the top-level graph and composite component:
g = SchematicGraph("chip", ps)
transmon = create_component(SimpleTransmon, ps, "components.transmon")
transmon_node = add_node!(g, transmon)The ParameterSet is preserved when graphs are copied — for example, inside BasicCompositeComponent or during _flatten operations. This means subcomponents at any depth can access the same parameter set.
Templates-Aliasing for Composite Subcomponents
The create_component(T, ps, address) pattern above hardcodes the subcomponent type at each call site. A composite can instead declare its subcomponents in a templates::NamedTuple field — the type and any designer-chosen defaults live on the composite, and ParameterSet values overlay on top of the template:
@compdef struct SimpleTransmonTemplated <: CompositeComponent
name = "transmon"
junction_gap = 12μm
templates = (
island = ExampleRectangleIsland(name="island", cap_width=30μm), # designer default
junction = ExampleSimpleJunction(name="junction"),
)
endUse the three-argument form of set_parameters inside _build_subcomponents to apply the ParameterSet at the subcomponent's address on top of the template. Trailing keyword arguments are composite-level overrides that win over the ParameterSet:
function SchematicDrivenLayout._build_subcomponents(tr::SimpleTransmonTemplated)
ps = parameter_set(tr._graph)
island = set_parameters(
tr.templates.island, ps, "components.$(name(tr)).island";
junction_gap=tr.junction_gap,
)
junction = set_parameters(
tr.templates.junction, ps, "components.$(name(tr)).junction";
h_ground_island=tr.junction_gap,
)
return (island, junction)
endPrecedence is explicit and layered:
- Template defaults — whatever
tr.templates.islandwas constructed with (e.g.cap_width=30μm). ParameterSetoverrides the template — every leaf undercomponents.transmon.islandoverwrites the matching template field.- Composite overrides the
ParameterSet— trailing kwargs toset_parameters(c, ps, address; …)always win, so composite invariants (e.g. "the island'sjunction_gapis whatever the composite decides") can never be silently undermined byParameterSetcontents.
Typos surface early: if the ParameterSet carries a leaf at components.transmon.island.fictional_param and ExampleRectangleIsland has no such field, set_parameters(tr.templates.island, ps, ...) throws ArgumentError naming the unknown leaf — rather than silently ignoring it or erroring later inside the constructor.
set_parameters(c, ps, address) also has a scoped-view sibling set_parameters(c, ps.components.transmon.island) for cases where the scoped ParameterSet is already in hand.
Access Tracking
The accessed field tracks which leaf parameters were read, enabling auditing of unused or missing parameters:
ps = ParameterSet()
ps.components.qubit.cap_width = 300μm
ps.components.qubit.cap_gap = 20μm
# Nothing accessed yet
isempty(ps.accessed) # => true
# Read a parameter — the fully qualified path is recorded
ps.components.qubit.cap_width # => 300μm
"components.qubit.cap_width" in ps.accessed # => true
# Tracking is shared across scoped views and still fully qualified
sub = ps.components.qubit
sub.cap_gap # => 20μm
"components.qubit.cap_gap" in ps.accessed # => true