diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index e0be1c1188..02deff803c 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -31,6 +31,7 @@ set to `false`. - `@equations`: for the list of equations - `@extend`: for extending a base system and unpacking its unknowns - `@icon` : for embedding the model icon + - `@metadata`: for assigning key-value pairs as model level metadata - `@parameters`: for specifying the symbolic parameters - `@structural_parameters`: for specifying non-symbolic parameters - `@variables`: for specifying the unknowns @@ -94,6 +95,16 @@ end v_for_defaults => 2.0 end end + +struct Author end +struct ModelVersion end +@mtkmodel ModelD begin + @description "A component with some metadata." + @metadata begin + Author = "Test Author" + ModelVersion = "1.0.0" + end +end ``` #### `@description` @@ -257,6 +268,30 @@ using ModelingToolkit end ``` +#### `@metadata` begin block + + - Assign key-value pairs as model level metadata. + - The keys must be `DataType` to avoid any key collisions. + - Assignments can be made using either `=` or `=>`. + - Metadata can be retrieved using [`getmetadata`](@ref). + - Metadata can be set using [`setmetadata`](@ref). + +```@example mtkmodel-example +using ModelingToolkit + +struct Author end +struct ModelVersion end + +@mtkmodel MetadataModel begin + @metadata begin + Author = "Test Author" + ModelVersion => "1.0.0" + end +end +@named model = MetadataModel() +getmetadata(model, Author, nothing) == "Test Author" +``` + #### A begin block - Any other Julia operations can be included with dedicated begin blocks. diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 61edc394b2..699cfee8fd 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -55,7 +55,8 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) dict = Dict{Symbol, Any}( :defaults => Dict{Symbol, Any}(), :kwargs => Dict{Symbol, Dict}(), - :structural_parameters => Dict{Symbol, Dict}() + :structural_parameters => Dict{Symbol, Dict}(), + :metadata => Dict{Symbol, Any}() ) comps = Union{Symbol, Expr}[] ext = [] @@ -127,6 +128,7 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) consolidate = get(dict, :consolidate, default_consolidate) description = get(dict, :description, "") + model_meta = get(dict, :metadata, Dict{Symbol, Any}()) @inline pop_structure_dict!.( Ref(dict), [:defaults, :kwargs, :structural_parameters]) @@ -145,6 +147,14 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) + meta_exprs = quote + for (k, v) in $model_meta + var"#___sys___" = setmetadata(var"#___sys___", $get_var($mod, k), v) + end + end + push!(exprs.args, meta_exprs) + push!(exprs.args, :(var"#___sys___")) + f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) else @@ -678,6 +688,8 @@ function parse_model!(exprs, comps, ext, eqs, icon, vs, ps, sps, c_evts, d_evts, parse_costs!(costs, dict, body) elseif mname == Symbol("@consolidate") parse_consolidate!(body, dict) + elseif mname == Symbol("@metadata") + parse_metadata_block!(body, dict, mod) else error("$mname is not handled.") end @@ -1254,6 +1266,21 @@ function parse_description!(body, dict) end end +function parse_metadata_block!(body, dict, mod) + Base.remove_linenums!(body) + for arg in body.args + MLStyle.@match arg begin + Expr(:(=), a, b) => begin + dict[:metadata][a] = get_var(mod, b) + end + Expr(:call, :(=>), a, b) => begin + dict[:metadata][a] = get_var(mod, b) + end + _ => error("Invalid metadata entry: $arg. Expected key = value or key => value format.") + end + end +end + ### Parsing Components: function component_args!(a, b, varexpr, kwargs; index_name = nothing) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index c48628b007..2c713d4149 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -1045,3 +1045,47 @@ end @test Example.structure[:constraints] == ["(EvalAt(0.3))(x) ~ 3", "y ≲ 4"] @test Example.structure[:costs] == ["x + y", "(EvalAt(1))(y) ^ 2"] end + +@testset "Model Level Metadata" begin + struct Author end + struct MyVersion end + struct License end + struct Category end + struct Tags end + struct MyBool end + struct NewInt end + + @mtkmodel TestMetadataModel begin + @metadata begin + Author = "Test Author" + MyVersion = "1.0.0" + License = "MIT" + Category => "example" + Tags = ["test", "demo", "metadata"] + MyBool => false + NewInt => 1 + end + + @parameters begin + k = 1.0 + end + + @variables begin + x(t) + y(t) + end + + @equations begin + D(x) ~ -k * x + y ~ x + end + end + @named test_model = TestMetadataModel() + + struct UnknownMetaKey end + @test ModelingToolkit.getmetadata(test_model, Author, nothing) == "Test Author" + @test ModelingToolkit.getmetadata(test_model, MyVersion, nothing) == "1.0.0" + @test ModelingToolkit.getmetadata(test_model, UnknownMetaKey, nothing) === nothing + @test ModelingToolkit.getmetadata(test_model, MyBool, nothing) === false + @test ModelingToolkit.getmetadata(test_model, NewInt, nothing) === 1 +end