diff --git a/Project.toml b/Project.toml index 87fc21e43..90336dc02 100644 --- a/Project.toml +++ b/Project.toml @@ -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" ThreadSafeDicts = "4239201d-c60e-5e0a-9702-85d713665ba7" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/src/schematics/SchematicDrivenLayout.jl b/src/schematics/SchematicDrivenLayout.jl index 18259d909..af43b43ec 100644 --- a/src/schematics/SchematicDrivenLayout.jl +++ b/src/schematics/SchematicDrivenLayout.jl @@ -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 @@ -137,6 +138,8 @@ include("components/variants.jl") include("solidmodels.jl") include("pdktools.jl") +include("parameter_extraction.jl") + include("ExamplePDK/ExamplePDK.jl") end # module diff --git a/src/schematics/parameter_extraction.jl b/src/schematics/parameter_extraction.jl new file mode 100644 index 000000000..e84d96347 --- /dev/null +++ b/src/schematics/parameter_extraction.jl @@ -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 + for sub_comp in components(comp) + _collect_defaults!(defaults, sub_comp) + end + end +end + +function _extract_node(comp::AbstractComponent, defaults::Dict) + 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 + 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) + 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) + 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" diff --git a/test/test_parameter_extraction.jl b/test/test_parameter_extraction.jl new file mode 100644 index 000000000..69e1270ed --- /dev/null +++ b/test/test_parameter_extraction.jl @@ -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 diff --git a/test/yaml_output/basics.yaml b/test/yaml_output/basics.yaml new file mode 100644 index 000000000..ff97840ef --- /dev/null +++ b/test/yaml_output/basics.yaml @@ -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 diff --git a/test/yaml_output/composite_hierarchy.yaml b/test/yaml_output/composite_hierarchy.yaml new file mode 100644 index 000000000..3a3db9323 --- /dev/null +++ b/test/yaml_output/composite_hierarchy.yaml @@ -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