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
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
Comment thread
abuzali-cqc marked this conversation as resolved.
ThreadSafeDicts = "4239201d-c60e-5e0a-9702-85d713665ba7"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
Expand Down
3 changes: 3 additions & 0 deletions src/schematics/SchematicDrivenLayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export @component,
replace_component!,
route!,
set_parameters
export extract_parameters, parameters_to_yaml
export ProcessTechnology, SimulationTarget, ArtworkTarget, SolidModelTarget
export base_variant, flipchip!, map_metadata!, @composite_variant, @variant

Expand Down Expand Up @@ -137,6 +138,8 @@ include("components/variants.jl")
include("solidmodels.jl")
include("pdktools.jl")

include("parameter_extraction.jl")

include("ExamplePDK/ExamplePDK.jl")

end # module
177 changes: 177 additions & 0 deletions src/schematics/parameter_extraction.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
extract_parameters(schematic::Schematic) -> Dict
extract_parameters(g::SchematicGraph) -> Dict

Walk the component hierarchy and return a dictionary with two top-level keys:
- `:defaults` — maps each component type name to its `default_parameters`
- `:components` — maps each node id to its parameters, with nested `:subcomponents`
"""
function extract_parameters(sch::Schematic)
return extract_parameters(sch.graph)
end

function extract_parameters(g::SchematicGraph)
defaults = Dict{String,Any}()
components_dict = Dict{String,Any}()

for node in nodes(g)
comp = component(node)
_collect_defaults!(defaults, comp)
components_dict[node.id] = _extract_node(comp, defaults)
end

return Dict("defaults" => defaults, "components" => components_dict)
end

function _collect_defaults!(defaults::Dict, comp::AbstractComponent)
type_name = string(nameof(typeof(comp)))
if !haskey(defaults, type_name)
defaults[type_name] = _params_to_dict(default_parameters(typeof(comp)))
end
if comp isa AbstractCompositeComponent
Comment thread
abuzali-cqc marked this conversation as resolved.
for sub_comp in components(comp)
_collect_defaults!(defaults, sub_comp)
end
end
end

function _extract_node(comp::AbstractComponent, defaults::Dict)
Comment thread
abuzali-cqc marked this conversation as resolved.
type_name = string(nameof(typeof(comp)))
entry = Dict{String,Any}(
"type" => type_name,
"parameters" => _params_to_dict(non_default_parameters(comp))
)
if comp isa AbstractCompositeComponent
Comment thread
abuzali-cqc marked this conversation as resolved.
subs = Dict{String,Any}()
for sub_node in nodes(graph(comp))
sub_comp = component(sub_node)
subs[sub_node.id] = _extract_node(sub_comp, defaults)
end
if !isempty(subs)
entry["subcomponents"] = subs
end
end
return entry
end

function _params_to_dict(params::NamedTuple)
d = Dict{String,Any}()
for (k, v) in pairs(params)
k === :name && continue
d[string(k)] = _format_value(v)
end
return d
end

function _format_value(v)
if v isa Unitful.Quantity
return string(v)
elseif v isa AbstractComponent
return string(nameof(typeof(v)))
elseif v isa Tuple
return [_format_value(x) for x in v]
elseif v isa AbstractArray
return [_format_value(x) for x in v]
elseif v isa NamedTuple
return _params_to_dict(v)
else
return v
end
end

# --- YAML serialization ---

"""
parameters_to_yaml(schematic; io=stdout)
parameters_to_yaml(g::SchematicGraph; io=stdout)

Extract parameters from a schematic and write YAML with anchored defaults
and merge-key-based component instances.
"""
function parameters_to_yaml(sch::Schematic; io::IO=stdout)
return parameters_to_yaml(sch.graph; io=io)
end

function parameters_to_yaml(g::SchematicGraph; io::IO=stdout)
data = extract_parameters(g)
_write_yaml(io, data)
end

function parameters_to_yaml(sch_or_graph, filename::AbstractString)
Comment thread
abuzali-cqc marked this conversation as resolved.
open(filename, "w") do io
parameters_to_yaml(sch_or_graph; io=io)
end
end

function _write_yaml(io::IO, data::Dict)
defaults = data["defaults"]
comps = data["components"]

println(io, "components:")

println(io, " default_parameters:")
for (type_name, params) in sort(collect(defaults); by=first)
anchor = _anchor_name(type_name)
println(io, " $type_name: &$anchor")
_write_yaml_dict(io, params, 6)
end

println(io)
for (node_id, entry) in sort(collect(comps); by=first)
println(io, " $node_id:")
_write_component_entry(io, entry, 4)
end
end

function _write_component_entry(io::IO, entry::Dict, indent::Int)
Comment thread
abuzali-cqc marked this conversation as resolved.
prefix = " " ^ indent
type_name = entry["type"]
anchor = _anchor_name(type_name)

println(io, prefix, "type: ", type_name)
println(io, prefix, "<<: *", anchor)

params = entry["parameters"]
if !isempty(params)
for (k, v) in sort(collect(params); by=first)
println(io, prefix, k, ": ", _yaml_value(v))
end
end

if haskey(entry, "subcomponents")
println(io, prefix, "subcomponents:")
for (sub_id, sub_entry) in sort(collect(entry["subcomponents"]); by=first)
println(io, prefix, " ", sub_id, ":")
_write_component_entry(io, sub_entry, indent + 4)
end
end
end

function _write_yaml_dict(io::IO, d::Dict, indent::Int)
prefix = " " ^ indent
for (k, v) in sort(collect(d); by=first)
println(io, prefix, k, ": ", _yaml_value(v))
end
end

function _yaml_value(v)
if v isa AbstractString
needs_quoting = occursin(r"[:#\[\]{}&*!|>'\",]", v) || v in ("true", "false", "null", "yes", "no")
return needs_quoting ? "\"$(escape_string(v))\"" : v
elseif v isa Bool
return v ? "true" : "false"
elseif v isa Number
return string(v)
elseif v isa AbstractVector
return "[" * join((_yaml_value(x) for x in v), ", ") * "]"
elseif v isa Dict
buf = IOBuffer()
println(buf)
_write_yaml_dict(buf, v, 0)
return String(take!(buf))
else
return repr(v)
end
end

_anchor_name(type_name::AbstractString) = type_name * "_defaults"
160 changes: 160 additions & 0 deletions test/test_parameter_extraction.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
@testsnippet ParamExtractionSetup begin
using Test
using DeviceLayout, Unitful
import Unitful: nm, μm, mm
using DeviceLayout.SchematicDrivenLayout

import DeviceLayout.SchematicDrivenLayout:
AbstractComponent,
AbstractCompositeComponent,
nodes,
component,
extract_parameters,
parameters_to_yaml

reset_uniquename!()

@compdef struct ExtTestLeaf <: AbstractComponent{typeof(1.0nm)}
name::String = "example_leaf_component"
width = 100nm
gap = 10nm
end
SchematicDrivenLayout.hooks(::ExtTestLeaf) = (
origin=PointHook(Point(0nm, 0nm), 0),
)

@compdef struct ExtTestComposite <: CompositeComponent
name::String = "example_composite_component"
leaf_width = 100nm
leaf_gap = 10nm
spacing = 500nm
end
SchematicDrivenLayout.hooks(::ExtTestComposite) = (
origin=PointHook(Point(0nm, 0nm), 0),
)

function SchematicDrivenLayout._build_subcomponents(c::ExtTestComposite)
@component leaf_a = ExtTestLeaf begin
width = c.leaf_width
gap = c.leaf_gap
end
@component leaf_b = ExtTestLeaf begin
width = c.leaf_width
end
return (leaf_a, leaf_b)
end

function SchematicDrivenLayout._graph!(
g::SchematicGraph,
::ExtTestComposite,
subcomps::NamedTuple
)
add_node!(g, subcomps.leaf_a)
add_node!(g, subcomps.leaf_b)
return nothing
end

SchematicDrivenLayout.map_hooks(::ExtTestComposite) =
Dict((1 => :origin) => :origin)

const tdir = mktempdir()
const save_yaml = get(ENV, "SAVE_YAML", "") == "1"
const yaml_outdir = joinpath(@__DIR__, "yaml_output")

function maybe_save_yaml(g::SchematicGraph, name::String)
save_yaml || return
mkpath(yaml_outdir)
outpath = joinpath(yaml_outdir, name * ".yaml")
parameters_to_yaml(g, outpath)
@info "Saved YAML to $outpath"
end
end

@testitem "extract_parameters basics" setup = [ParamExtractionSetup] begin
g = SchematicGraph("test_extraction")
leaf = ExtTestLeaf(width=200nm)
node = add_node!(g, leaf)

data = extract_parameters(g)

# Defaults section has the type
@test haskey(data["defaults"], "ExtTestLeaf")
defs = data["defaults"]["ExtTestLeaf"]
@test defs["width"] == string(100nm)
@test defs["gap"] == string(10nm)

# Instance only has overrides
entry = data["components"][node.id]
@test entry["type"] == "ExtTestLeaf"
@test entry["parameters"]["width"] == string(200nm)
@test !haskey(entry["parameters"], "gap")

maybe_save_yaml(g, "basics")
end

@testitem "extract_parameters composite hierarchy" setup = [ParamExtractionSetup] begin
g = SchematicGraph("test_composite")
comp = ExtTestComposite(leaf_width=300nm)
node = add_node!(g, comp)

data = extract_parameters(g)

# Both types should appear in defaults
@test haskey(data["defaults"], "ExtTestComposite")
@test haskey(data["defaults"], "ExtTestLeaf")

# Top-level instance has override
entry = data["components"][node.id]
@test entry["type"] == "ExtTestComposite"
@test entry["parameters"]["leaf_width"] == string(300nm)

# Subcomponents present
@test haskey(entry, "subcomponents")
@test length(entry["subcomponents"]) == 2

maybe_save_yaml(g, "composite_hierarchy")
end

@testitem "parameters_to_yaml output" setup = [ParamExtractionSetup] begin
g = SchematicGraph("test_yaml")
add_node!(g, ExtTestLeaf(width=200nm))
add_node!(g, ExtTestComposite(leaf_width=300nm, spacing=1000nm))

buf = IOBuffer()
parameters_to_yaml(g; io=buf)
yaml_str = String(take!(buf))

# Check structure
@test occursin("components:", yaml_str)
@test occursin("default_parameters:", yaml_str)

# Check anchors and merge keys
@test occursin("&ExtTestLeaf_defaults", yaml_str)
@test occursin("*ExtTestLeaf_defaults", yaml_str)
@test occursin("<<: *ExtTestLeaf_defaults", yaml_str)

# Check type field present
@test occursin("type: ExtTestLeaf", yaml_str)
@test occursin("type: ExtTestComposite", yaml_str)

# Check overrides show up
@test occursin(string(200nm), yaml_str)
@test occursin(string(300nm), yaml_str)

maybe_save_yaml(g, "yaml_output")
end

@testitem "parameters_to_yaml file output" setup = [ParamExtractionSetup] begin
g = SchematicGraph("test_yaml_file")
add_node!(g, ExtTestLeaf(width=500nm))

filepath = joinpath(tdir, "test_params.yaml")
parameters_to_yaml(g, filepath)

@test isfile(filepath)
content = read(filepath, String)
@test occursin("components:", content)
@test occursin("default_parameters:", content)

maybe_save_yaml(g, "file_output")
end
11 changes: 11 additions & 0 deletions test/yaml_output/basics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
components:
default_parameters:
ExtTestLeaf: &ExtTestLeaf_defaults
gap: 10 nm
name: leaf
width: 100 nm

leaf:
type: ExtTestLeaf
<<: *ExtTestLeaf_defaults
width: 200 nm
23 changes: 23 additions & 0 deletions test/yaml_output/composite_hierarchy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
components:
default_parameters:
ExtTestComposite: &ExtTestComposite_defaults
leaf_gap: 10 nm
leaf_width: 100 nm
spacing: 500 nm
ExtTestLeaf: &ExtTestLeaf_defaults
gap: 10 nm
width: 100 nm

example_composite_component:
type: ExtTestComposite
<<: *ExtTestComposite_defaults
leaf_width: 300 nm
subcomponents:
leaf_a:
type: ExtTestLeaf
<<: *ExtTestLeaf_defaults
width: 300 nm
leaf_b:
type: ExtTestLeaf
<<: *ExtTestLeaf_defaults
width: 300 nm
Loading