Fix ParameterSet round trip and @variant keyword handling - #338
parrangoiz wants to merge 9 commits into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| ps = parameter_set(c._graph) | ||
| isnothing(ps) && return create_component(typeof(c), name, params; kwargs...) |
There was a problem hiding this comment.
This still leads to missing parameters set as it's never passed from the composite component parent:
@compdef struct MWEComposite <: CompositeComponent
name = "t"
end
function SchematicDrivenLayout._build_subcomponents(c::MWEComposite)
ps = parameter_set(c._graph)
ps === nothing && error("_build_subcomponents: no ParameterSet on _graph")
return (create_component(ExampleRectangleIsland, ps, "components.t.island"),)
end
ps1 = ParameterSet(); ps1.components.t.island.cap_width = 42μm
ps2 = ParameterSet(); ps2.components.t.island.cap_width = 99μm
# (a) stale set
tr = create_component(MWEComposite, ps1, "components.t")
tr2 = set_parameters(tr, ps2, "components.t")
parameter_set(tr2._graph) === ps2 # false — still ps1
# (b) no set at all
tr3 = set_parameters(MWEComposite(), ps2, "components.t")
parameter_set(tr3._graph) === ps2 # false — nothing| return (isempty(kw) ? (;) : NamedTuple(kw)), paths | ||
| end | ||
|
|
||
| function _namespace_to_namedtuple!(paths::Vector{String}, d::Dict, path::String) |
There was a problem hiding this comment.
A typo inside a nested NamedTuple namespace is accepted silently. For a component with style = (; trace=10μm, gap=6μm):
ps = ParameterSet()
ps.components.line.style.trce = 3μm # meant `trace`
create_component(MWELine, ps, "components.line").style
# (trace = 10 μm, gap = 6 μm, trce = 3 μm)
set_parameters(MWELine(), ps, "components.line").style
# same
"components.line.style.trce" in ps.accessed # trueNothing errors, trace keeps its default, and the typo is recorded in ps.accessed, so an unused-parameter check won't catch it either. A top-level typo (ps.components.line.lenght) already throws an ArgumentError, which is what I'd expect here too.
I would suggest to pass the default or template value down from _namedtuple_namespaces (it already has base[s]), and throw an ArgumentError naming the full path and the valid fields when a key isn't in it, at every nesting level.
| ) | ||
| ) | ||
| kw = leaf_params(sub) | ||
| nested_kw, nested_paths = _namedtuple_namespaces(sub, default_parameters(T)) |
There was a problem hiding this comment.
A namespace for a required (no-default) NamedTuple or Dict parameter gets dropped here, and the constructor then fails with an error that doesn't mention the namespace:
@compdef struct MWEReq <: Component
name = "req"
style::NamedTuple
end
ps = ParameterSet()
ps.components.req.style.trace = 5μm
create_component(MWEReq, ps, "components.req")
# UndefKeywordError: keyword argument `style` not assignedIt fails the same way for labels::Dict{String, Any}, and passing the value directly (MWEReq(; style=(; trace=5μm))) works.
The cause is that default_parameters(T) only has parameters with defaults, so haskey(base, s) in _namedtuple_namespaces (line 195) is false for style. The namespace is then skipped like any other namespace that isn't a parameter.
So for names in parameter_names(T) that aren't in default_parameters(T), you should check if component fields have corresponding branch in the NamedTuple or Dict.
| params::NamedTuple=parameters(c); | ||
| kwargs... | ||
| ) | ||
| ps = parameter_set(c._graph) |
There was a problem hiding this comment.
This line assumes every AbstractCompositeComponent has a _graph field, but BasicCompositeComponent stores its graph in graph. The lookup falls through getproperty to the inner MetaGraph, so on this branch:
g = SchematicGraph("g")
add_node!(g, ExampleRectangleIsland())
bcc = BasicCompositeComponent(g)
set_parameters(bcc)
# FieldError: type MetaGraphs.MetaGraph has no field `_graph`, available fields: `graph`, `vprops`, ...I think we need graph(c) defined for every composite, so ps = parameter_set(graph(c)) avoids the field lookup. The catch is that for @compdef composites, graph(c) builds the subcomponents, so it isn't a drop-in change. The simpler option is to guard with hasfield(typeof(c), :_graph) and use the generic method otherwise.
| if v isa Dict | ||
| push!(fields, Symbol(k) => _namespace_to_namedtuple!(paths, v, subpath)) |
There was a problem hiding this comment.
The round trip breaks when a Dict is nested inside a NamedTuple parameter: extract_parameter_set writes it out, but it comes back as a NamedTuple.
@compdef struct MWEStyled <: Component
name = "line"
style = (; trace=10μm, extra=Dict{String, Any}("k" => 1))
end
g = SchematicGraph("g")
add_node!(g, MWEStyled())
ps = extract_parameter_set(g)
c = create_component(MWEStyled, ps, "components.line")
typeof(MWEStyled().style.extra) # Dict{String, Any}
typeof(c.style.extra) # @NamedTuple{k::Int64}_extracted_parameter_value writes NamedTuples and Dicts the same way, as a nested Dict{String, Any}, so the namespace alone can't tell them apart. This branch then turns every nested Dict into a NamedTuple.
We should pass down the parameter's default or template value right type at each level. Recurse here when the matching field is a NamedTuple, and call _namespace_to_dict! when it's an AbstractDict.
| function _check_variant_kwargs(::Type{T}, kwargs) where {T} | ||
| names = parameter_names(T) | ||
| all(in(names), keys(kwargs)) && return nothing | ||
| throw(MethodError(Core.kwcall, ((; kwargs...), T), Base.get_world_counter())) |
There was a problem hiding this comment.
I think ArgumentError is better suited here then MethodError which requires non-public get_world_counter.
throw(ArgumentError("unknown keyword(s) for $T; valid parameters are ..."))create_component(T, ps, address) and set_parameters(c, ps, address) passed only the leaves at address, so the nested namespaces that extract_parameter_set writes for NamedTuple parameters were dropped. A nested namespace whose key names a NamedTuple-valued parameter is now converted to a NamedTuple and merged recursively into the default/template value, and its leaves are recorded in ps.accessed.
@compdef constructors throw a MethodError on unknown keywords, but @variant and @composite_variant constructors merged every keyword into their parameter NamedTuple, so typos (including ones in a ParameterSet namespace passed to create_component) were silently stored as new parameters. Variant constructors now throw the same MethodError for keywords that are not parameters of the variant. Composite-internal fields (_graph, _schematic, _hooks) are exempt, as the base constructor accepts them.
create_component(T, ps, address) attaches ps to the composite's _graph, but a @composite_variant constructor always built its own SchematicGraph and stored the _graph keyword as a parameter, and base_variant built the base composite with a fresh graph, so _build_subcomponents never saw the ParameterSet. The variant constructor now accepts _graph, _schematic and _hooks keywords like a @compdef constructor, and base_variant attaches the variant's ParameterSet to the base composite's graph.
… the PS through set_parameters Three follow-ups to #335/#337 found while porting a design to the ParameterSet API: - _namedtuple_namespaces silently skipped a nested namespace whose key names a parameter that is not NamedTuple-valued, so a shape mistake in the source (e.g. a list parameter written as a mapping) kept the code default without any error. A namespace naming a Dict-valued parameter is now read back as a Dict with the parameter's key type (the shape extract_parameter_set writes for Dict parameters), and one naming a parameter of any other type throws an ArgumentError. Namespaces whose key is not a parameter (composite subcomponents) are still ignored. - create_component(T, ps, address) with an empty address or one resolving to a leaf value fell through to a generic MethodError (or, for composites, to a failing ParameterKeyError construction). Both address forms now share _resolve_namespace with set_parameters(c, ps, address): ParameterKeyError for a missing address, ArgumentError with an actionable message for an empty or leaf address. - set_parameters on a composite rebuilt it through the constructor, whose fresh graph carried no ParameterSet, silently undoing what create_component(T, ps, address) had attached; _build_subcomponents of the new instance then saw nothing. The composite method now attaches the same ParameterSet to the new graph (same uniquename count).
…ocstrings Describe only the outward-facing behavior of each method, drop implementation details, error-handling walkthroughs and comparisons with other functions, and show the nested-namespace behavior in Examples blocks. The composite create_component method no longer has its own docstring; its user-facing behavior is covered by the address-form docstring.
4ba6e06 to
735326f
Compare
Fixes #335, #336, #337
Fixes to how
ParameterSets feed components and variants.create_component(T, ps, address)andset_parameters(c, ps, address)now read a nested namespace belowaddressback as the parameter it names, so whatextract_parameter_setwrites can be read back:NamedTupleparameter is merged recursively into the default or template value, so the namespace only needs the fields that change.Dictparameter is replaced by the namespace's entries, with keys converted to the parameter's key type.ArgumentError. Previously it silently kept the default.ps.accessed.@variantand@composite_variantconstructors throw the sameMethodErroras the base@compdefconstructor for keywords that aren't parameters of the variant. Previously a typo, including one in aParameterSetnamespace, was silently stored as a new parameter.@composite_variantbuilt withcreate_component(T, ps, address)now carriespsin its graph, so the base composite's_build_subcomponentssees it throughparameter_set(cc._graph). The variant constructor accepts_graph,_schematicand_hookslike a@compdefconstructor instead of storing them as parameters.base_variantattaches the sameParameterSetto the base composite's graph.create_component(T, ps, address)with an empty address, or one that resolves to a leaf value, now throws anArgumentErrorwith an actionable message, asset_parameters(c, ps, address)already did. Previously it threw a genericMethodError, or for composites failed while building aParameterKeyError.set_parameterson a composite keeps theParameterSetattached to its graph. Previously the rebuilt instance's_build_subcomponentsno longer saw it.Docstrings, the
ParameterSettutorial and the CHANGELOG are updated.Also cleaned up the docstrings of
create_component,set_parameters(including the callable formc(name, params; kwargs...)) andextract_parameter_setto make them user friendly: they now describe only the outward-facing behavior, with examples.Testing
New tests in
test/test_parameter_set.jlandtest/test_schematicdriven.jl; no existing tests changed.