Skip to content
Open
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
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ gmsh_jll = "630162c2-fc9b-58b3-9910-8442a8a132e6"

[weakdeps]
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"

[extensions]
ParameterSetYAMLExt = "YAML"
SchematicGraphMakieExt = "GraphMakie"

[compat]
Expand Down Expand Up @@ -76,13 +78,15 @@ StaticArrays = "1"
TestItemRunner = "1.1.0"
ThreadSafeDicts = "0.1"
Unitful = "1.2"
YAML = "0.4"
gmsh_jll = "4.13"
julia = "1.10"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"

[targets]
test = ["Aqua", "Test", "TestItemRunner"]
test = ["Aqua", "Test", "TestItemRunner", "YAML"]
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ makedocs(
"Geometry API Reference" => "reference/api.md",
"Path API Reference" => "reference/path_api.md",
"Schematic API Reference" => "reference/schematic_api.md",
"ParameterSet API Reference" => "reference/parameter_set.md",
"Shape Reference" => "reference/shapes.md"
],
"FAQ/Troubleshooting" => "how_to/faq.md"
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Docstrings are split up over a few pages:
- [Geometry-Level Layout API](api.md)
- [Path API](path_api.md)
- [Schematic-Driven Design API](schematic_api.md)
- [ParameterSet API Reference](parameter_set.md)
- [Shape Library](shapes.md)

Or jump straight to a particular topic:
Expand Down
264 changes: 264 additions & 0 deletions docs/src/reference/parameter_set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# ParameterSet

A `ParameterSet` is a mutable parameter source for data-driven design. It holds a nested dictionary of parameters - typically loaded from a YAML file - and provides dot-access syntax for reading and writing values.

Every `ParameterSet` contains two required top-level namespaces:
- **`global`** - parameters shared across the design (e.g., version, process node)
- **`components`** - per-component parameter trees

## Tutorial: Using External Parameters

This tutorial shows how to drive a design from a `ParameterSet` instead of
hardcoding parameter values. We start with simple components, then move to
composite components.

### Setup

```julia
using DeviceLayout, .PreferredUnits
using DeviceLayout.SchematicDrivenLayout
```

### Creating a ParameterSet

The simplest way is to build one programmatically:

```julia
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:

```yaml
# 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
```

```julia
using YAML # activates the ParameterSetYAMLExt extension
ps = ParameterSet("design_params.yaml")
```

### Reading Parameters

Use dot syntax to navigate the hierarchy:

```julia
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:

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

Extract all leaf parameters at a level as a `NamedTuple`:

```julia
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`:

```julia
@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`:

```julia
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:

```julia
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:

```julia
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:

```julia
# 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
@component cap1 = create_component(MyCapacitor, ps, "components.cap1")
@component 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:

```julia
@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`:

```julia
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 composite instance and
forwarded to both subcomponents under their respective parameter names:

```julia
function SchematicDrivenLayout._build_subcomponents(tr::SimpleTransmon)
ps = parameter_set(tr._graph)

@component island = create_component(
ExampleRectangleIsland, ps, "components.transmon.island"
Copy link
Copy Markdown
Member

@gpeairs gpeairs Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be ”components.$(name(tr)).island" in general?

)
# Forward shared parameter from parameter set to island
island = set_parameters(island; junction_gap=ps.components.transmon.junction_gap)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's up to the component definition which of the ParameterSet or derived parameter takes precedence. The PS won't reflect any changes applied by set_parameters. My understanding is that's intentional (PS is just input parameters, final parameters can be extracted later and compared if necessary), but just want to double check.


@component junction = create_component(
ExampleSimpleJunction, ps, "components.transmon.junction"
)
# Forward shared parameter under the subcomponent's own name
junction = set_parameters(junction; h_ground_island=ps.components.transmon.junction_gap)
Copy link
Copy Markdown
Member

@gpeairs gpeairs Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you think of applying the parameter set automatically in add_node!, since it's now present in the graph? Then we wouldn't require any changes to the component definition, and we could also avoid ambiguity about precedence.

If we only do that, then the designer can still forward whatever they like in _build_subcomponents, but anything present in the ParameterSet would be overridden. So we would lose any designer-intended consistency if the PS overrides something that’s supposed to be dependent.

Alternatively, if we want to enforce consistency, we can rely on the designer following the templates pattern to allow setting arbitrary subcomponent parameters. Then to set them via the PS, we just say subcomponent addresses in the PS are just aliases for the subcomponent templates. In other words, components.transmon.island really overrides components.transmon.templates.island. PS overrides the templates, then _build_subcomponents always overrides the PS to enforce CompositeComponent invariants. I'm not committed to that path, but it seems preferable to relying on the designer to correctly apply the ParameterSet in _build_subcomponents.

In that case, we wouldn't even need the CompositeComponent to hold onto the PS after creation.


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:

```julia
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.

### Access Tracking

The `accessed` field tracks which leaf parameters were read, enabling auditing
of unused or missing parameters:

```julia
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
ps.components.qubit.cap_width # => 300μm
"cap_width" in ps.accessed # => true

# Tracking is shared across scoped views
sub = ps.components.qubit
sub.cap_gap # => 20μm
"cap_gap" in ps.accessed # => true
```

## API Reference

```@docs
DeviceLayout.ParameterSet
DeviceLayout.resolve
DeviceLayout.leaf_params
```
74 changes: 74 additions & 0 deletions ext/ParameterSetYAMLExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module ParameterSetYAMLExt

import DeviceLayout: ParameterSet
import DeviceLayout
import YAML
import Unitful

"""
_parse_units!(data::Dict{String, Any})

Recursively walk a parsed YAML dict. String values parseable by `Unitful.uparse`
(e.g. `"150μm"`) are converted to `Unitful.Quantity` values.
"""
function _parse_units!(data::Dict{String, Any})
for (k, v) in data
if v isa Dict{String, Any}
_parse_units!(v)
elseif v isa AbstractString
try
data[k] = Unitful.uparse(v)
catch
# not a valid unit expression, keep as-is
end
end
end
return data
end

"""
_serialize_units(data::Dict{String, Any}) -> Dict{String, Any}

Return a deep copy of `data` with `Unitful.Quantity` values converted to
strings like `"150μm"` (no space, round-trips through `Unitful.uparse`).
"""
function _serialize_units(data::Dict{String, Any})
out = Dict{String, Any}()
for (k, v) in data
if v isa Dict{String, Any}
out[k] = _serialize_units(v)
elseif v isa Unitful.Quantity
out[k] = "$(Unitful.ustrip(v))$(Unitful.unit(v))"
else
out[k] = v
end
end
return out
end

function ParameterSet(io::IO, path::String="")
data = YAML.load(io; dicttype=Dict{String, Any})
_parse_units!(data)
return DeviceLayout.ParameterSet(path, data)
end

function ParameterSet(path::String)
return open(path) do io
DeviceLayout.ParameterSet(io, path)
end
end

function DeviceLayout.save_parameter_set(io::IO, ps::ParameterSet)
data = _serialize_units(getfield(ps, :data))
YAML.write(io, data)
return io
end

function DeviceLayout.save_parameter_set(path::String, ps::ParameterSet)
open(path, "w") do io
DeviceLayout.save_parameter_set(io, ps)
end
return path
end

end # module ParameterSetYAMLExt
3 changes: 3 additions & 0 deletions src/DeviceLayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,9 @@ export PolyText,
referenced_characters_demo,
scripted_demo

include("parameter_set.jl")
export ParameterSet, MissingNamespace, ParameterKeyError

include("schematics/SchematicDrivenLayout.jl")
export SchematicDrivenLayout

Expand Down
Loading
Loading