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 ParameterSet programmatically or from YAML
  • Reading values with dot syntax or resolve
  • Instantiating components from a ParameterSet
  • Attaching a ParameterSet to a SchematicGraph
  • Forwarding shared parameters into composite subcomponents
  • Auditing which parameters were actually consumed

Prerequisites

Setup

using DeviceLayout, .PreferredUnits
using DeviceLayout.SchematicDrivenLayout

Every 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μm

If 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μm
using 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μm

Or use resolve with a dot-separated address:

resolve(ps, "components.capacitor.finger_length")  # => 150μm
resolve(ps, "components.capacitor")                 # => scoped ParameterSet

Extract 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
end

You 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μm

A 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
end

Define 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μm

Inside _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)
end

create_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"),
    )
end

Use 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)
end

Precedence is explicit and layered:

  1. Template defaults — whatever tr.templates.island was constructed with (e.g. cap_width=30μm).
  2. ParameterSet overrides the template — every leaf under components.transmon.island overwrites the matching template field.
  3. Composite overrides the ParameterSet — trailing kwargs to set_parameters(c, ps, address; …) always win, so composite invariants (e.g. "the island's junction_gap is whatever the composite decides") can never be silently undermined by ParameterSet contents.

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