From d47af77e253e7d8dcb6d9e05d65d1b94d2dee23c Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:52:57 +0530 Subject: [PATCH 1/5] feat: support array of components in `@mtkmodel` - for loop or a list comprehension can be used to declare component arrays --- src/systems/abstractsystem.jl | 7 +- src/systems/model_parsing.jl | 132 ++++++++++++++++++++++------------ test/model_parsing.jl | 38 ++++++++++ 3 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e37cb874c2..253565ea88 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1402,7 +1402,7 @@ function _named(name, call, runtime = false) end end -function _named_idxs(name::Symbol, idxs, call) +function _named_idxs(name::Symbol, idxs, call; extra_args = "") if call.head !== :-> throw(ArgumentError("Not an anonymous function")) end @@ -1413,7 +1413,10 @@ function _named_idxs(name::Symbol, idxs, call) ex = Base.Cartesian.poplinenum(ex) ex = _named(:(Symbol($(Meta.quot(name)), :_, $sym)), ex, true) ex = Base.Cartesian.poplinenum(ex) - :($name = $map($sym -> $ex, $idxs)) + :($name = map($sym -> begin + $extra_args + $ex + end, $idxs)) end function single_named_expr(expr) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 67abc2cca9..646aa9b888 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -40,7 +40,7 @@ function _model_macro(mod, name, expr, isconnector) :kwargs => Dict{Symbol, Dict}(), :structural_parameters => Dict{Symbol, Dict}() ) - comps = Symbol[] + comps = Union{Symbol, Expr}[] ext = Ref{Any}(nothing) eqs = Expr[] icon = Ref{Union{String, URI}}() @@ -745,7 +745,7 @@ end ### Parsing Components: -function component_args!(a, b, expr, varexpr, kwargs) +function component_args!(a, b, varexpr, kwargs; index_name = nothing) # Whenever `b` is a function call, skip the first arg aka the function name. # Whenever it is a kwargs list, include it. start = b.head == :call ? 2 : 1 @@ -754,73 +754,115 @@ function component_args!(a, b, expr, varexpr, kwargs) arg isa LineNumberNode && continue MLStyle.@match arg begin x::Symbol || Expr(:kw, x) => begin - _v = _rename(a, x) - b.args[i] = Expr(:kw, x, _v) - push!(varexpr.args, :((@isdefined $x) && ($_v = $x))) - push!(kwargs, Expr(:kw, _v, nothing)) - # dict[:kwargs][_v] = nothing + varname, _varname = _rename(a, x) + b.args[i] = Expr(:kw, x, _varname) + push!(varexpr.args, :((if $varname !== nothing + $_varname = $varname + elseif @isdefined $x + # Allow users to define a var in `structural_parameters` and set + # that as positional arg of subcomponents; it is useful for cases + # where it needs to be passed to multiple subcomponents. + $_varname = $x + end))) + push!(kwargs, Expr(:kw, varname, nothing)) + # dict[:kwargs][varname] = nothing end Expr(:parameters, x...) => begin - component_args!(a, arg, expr, varexpr, kwargs) + component_args!(a, arg, varexpr, kwargs) end Expr(:kw, x, y) => begin - _v = _rename(a, x) - b.args[i] = Expr(:kw, x, _v) - push!(varexpr.args, :($_v = $_v === nothing ? $y : $_v)) - push!(kwargs, Expr(:kw, _v, nothing)) - # dict[:kwargs][_v] = nothing + varname, _varname = _rename(a, x) + b.args[i] = Expr(:kw, x, _varname) + if isnothing(index_name) + push!(varexpr.args, :($_varname = $varname === nothing ? $y : $varname)) + else + push!(varexpr.args, + :($_varname = $varname === nothing ? $y : $varname[$index_name])) + end + push!(kwargs, Expr(:kw, varname, nothing)) + # dict[:kwargs][varname] = nothing end _ => error("Could not parse $arg of component $a") end end end -function _parse_components!(exprs, body, kwargs) - expr = Expr(:block) +model_name(name, range) = Symbol.(name, :_, collect(range)) + +function _parse_components!(body, kwargs) + local expr varexpr = Expr(:block) - # push!(exprs, varexpr) - comps = Vector{Union{Symbol, Expr}}[] + comps = Vector{Union{Union{Expr, Symbol}, Expr}}[] comp_names = [] - for arg in body.args - arg isa LineNumberNode && continue - MLStyle.@match arg begin - Expr(:block) => begin - # TODO: Do we need this? - error("Multiple `@components` block detected within a single block") - end - Expr(:(=), a, b) => begin - arg = deepcopy(arg) - b = deepcopy(arg.args[2]) + Base.remove_linenums!(body) + arg = body.args[end] + + MLStyle.@match arg begin + Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:(=), c, d)))) => begin + array_varexpr = Expr(:block) - component_args!(a, b, expr, varexpr, kwargs) + push!(comp_names, :($a...)) + push!(comps, [a, b.args[1], d]) + b = deepcopy(b) - arg.args[2] = b - push!(expr.args, arg) - push!(comp_names, a) + component_args!(a, b, array_varexpr, kwargs; index_name = c) + + expr = _named_idxs(a, d, :($c -> $b); extra_args = array_varexpr) + end + Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:filter, e, Expr(:(=), c, d))))) => begin + error("List comprehensions with conditional statements aren't supported.") + end + Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:(=), c, d), e...))) => begin + # Note that `e` is of the form `Tuple{Expr(:(=), c, d)}` + error("More than one index isn't supported while building component array") + end + Expr(:block) => begin + # TODO: Do we need this? + error("Multiple `@components` block detected within a single block") + end + Expr(:(=), a, Expr(:for, Expr(:(=), c, d), b)) => begin + Base.remove_linenums!(b) + array_varexpr = Expr(:block) + push!(array_varexpr.args, b.args[1:(end - 1)]...) + push!(comp_names, :($a...)) + push!(comps, [a, b.args[end].args[1], d]) + b = deepcopy(b) + + component_args!(a, b.args[end], array_varexpr, kwargs; index_name = c) + + expr = _named_idxs(a, d, :($c -> $(b.args[end])); extra_args = array_varexpr) + end + Expr(:(=), a, b) => begin + arg = deepcopy(arg) + b = deepcopy(arg.args[2]) + + component_args!(a, b, varexpr, kwargs) + + arg.args[2] = b + expr = :(@named $arg) + push!(comp_names, a) if (isa(b.args[1], Symbol) || Meta.isexpr(b.args[1], :.)) - push!(comps, [a, b.args[1]]) + push!(comps, [a, b.args[1]]) end - end - _ => error("Couldn't parse the component body: $arg") end + _ => error("Couldn't parse the component body: $arg") end + return comp_names, comps, expr, varexpr end function push_conditional_component!(ifexpr, expr_vec, comp_names, varexpr) blk = Expr(:block) push!(blk.args, varexpr) - push!(blk.args, :(@named begin - $(expr_vec.args...) - end)) + push!(blk.args, expr_vec) push!(blk.args, :($push!(systems, $(comp_names...)))) push!(ifexpr.args, blk) end function handle_if_x!(mod, exprs, ifexpr, x, kwargs, condition = nothing) push!(ifexpr.args, condition) - comp_names, comps, expr_vec, varexpr = _parse_components!(ifexpr, x, kwargs) + comp_names, comps, expr_vec, varexpr = _parse_components!(x, kwargs) push_conditional_component!(ifexpr, expr_vec, comp_names, varexpr) comps end @@ -836,7 +878,7 @@ function handle_if_y!(exprs, ifexpr, y, kwargs) push!(ifexpr.args, elseifexpr) (comps...,) else - comp_names, comps, expr_vec, varexpr = _parse_components!(exprs, y, kwargs) + comp_names, comps, expr_vec, varexpr = _parse_components!(y, kwargs) push_conditional_component!(ifexpr, expr_vec, comp_names, varexpr) comps end @@ -861,25 +903,23 @@ function parse_components!(exprs, cs, dict, compbody, kwargs) Expr(:if, condition, x, y) => begin handle_conditional_components(condition, dict, exprs, kwargs, x, y) end - Expr(:(=), a, b) => begin - comp_names, comps, expr_vec, varexpr = _parse_components!(exprs, - :(begin + # Either the arg is top level component declaration or an invalid cause - both are handled by `_parse_components` + _ => begin + comp_names, comps, expr_vec, varexpr = _parse_components!(:(begin $arg end), kwargs) push!(cs, comp_names...) push!(dict[:components], comps...) - push!(exprs, varexpr, :(@named begin - $(expr_vec.args...) - end)) + push!(exprs, varexpr, expr_vec) end - _ => error("Couldn't parse the component body $compbody") end end end function _rename(compname, varname) compname = Symbol(compname, :__, varname) + (compname, Symbol(:_, compname)) end # Handle top level branching diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 706b97c598..851aa33aab 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -650,3 +650,41 @@ end @named m = MyModel() @variables x___(t) @test isequal(x___, _b[]) + +@testset "Component array" begin + @mtkmodel SubComponent begin + @parameters begin + sc + end + end + + @mtkmodel Component begin + @structural_parameters begin + N = 2 + end + @components begin + comprehension = [SubComponent(sc = i) for i in 1:N] + written_out_for = for i in 1:N + sc = i + 1 + SubComponent(; sc) + end + single_sub_component = SubComponent() + end + end + + @named component = Component() + component = complete(component) + + @test nameof.(ModelingToolkit.get_systems(component)) == [ + :comprehension_1, + :comprehension_2, + :written_out_for_1, + :written_out_for_2, + :single_sub_component, + ] + + @test getdefault(component.comprehension_1.sc) == 1 + @test getdefault(component.comprehension_2.sc) == 2 + @test getdefault(component.written_out_for_1.sc) == 2 + @test getdefault(component.written_out_for_2.sc) == 3 +end From 86d345fbf4c7b35c2fbcc5797b88ab5e9d53065a Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:45:45 +0530 Subject: [PATCH 2/5] docs: add component array in the docs --- docs/src/basics/MTKModel_Connector.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/src/basics/MTKModel_Connector.md b/docs/src/basics/MTKModel_Connector.md index c9fc0bcb60..ef17518ff4 100644 --- a/docs/src/basics/MTKModel_Connector.md +++ b/docs/src/basics/MTKModel_Connector.md @@ -58,6 +58,7 @@ end end @structural_parameters begin f = sin + N = 2 end begin v_var = 1.0 @@ -69,6 +70,11 @@ end @extend ModelB(; p1) @components begin model_a = ModelA(; k_array) + model_array_a = [ModelA(; k = i) for i in 1:N] + model_array_b = for i in 1:N + k = i^2 + ModelA(; k) + end end @equations begin model_a.k ~ f(v) @@ -146,6 +152,7 @@ julia> @mtkbuild model_c2 = ModelC(; p1 = 2.0) #### `@components` begin block - Declare the subcomponents within `@components` begin block. + - Array of components can be declared with a for loop or a list comprehension. - The arguments in these subcomponents are promoted as keyword arguments as `subcomponent_name__argname` with `nothing` as default value. - Whenever components are created with `@named` macro, these can be accessed with `.` operator as `subcomponent_name.argname` - In the above example, as `k` of `model_a` isn't listed while defining the sub-component in `ModelC`, its default value can't be modified by users. While `k_array` can be set as: @@ -223,7 +230,7 @@ end ``` !!! note - + For more examples of usage, checkout [ModelingToolkitStandardLibrary.jl](https://github.com/SciML/ModelingToolkitStandardLibrary.jl/) ## More on `Model.structure` @@ -247,14 +254,13 @@ For example, the structure of `ModelC` is: ```julia julia> ModelC.structure Dict{Symbol, Any} with 9 entries: - :components => Any[Union{Expr, Symbol}[:model_a, :ModelA]] + :components => Any[Union{Expr, Symbol}[:model_a, :ModelA], Union{Expr, Symbol}[:model_array_a, :ModelA, :(1:N)], Union{Expr, Symbol}[:model_array_b, :ModelA, :(1:N)]] :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_array=>Dict(:type=>Real, :size=>(2, 3))) :icon => URI("https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png") - :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) - :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :v=>Dict{Symbol, Union{Nothing, Symbol}}(:value=>:v_var, :type=>Real), :v_array=>Dict(:value=>nothing, :type=>Real), :p1=>Dict(:value=>nothing)) - :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin)) + :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2), :v=>Dict{Symbol, Any}(:value=>:v_var, :type=>Real), :v_array=>Dict{Symbol, Union{Nothing, UnionAll}}(:value=>nothing, :type=>AbstractArray{Real}), :p1=>Dict(:value=>nothing)) + :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2)) :independent_variable => t - :constants => Dict{Symbol, Dict}(:c=>Dict(:value=>1)) + :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) :extend => Any[[:p2, :p1], Symbol("#mtkmodel__anonymous__ModelB"), :ModelB] :equations => Any["model_a.k ~ f(v)"] ``` From 53fabfc2b9e72a9a4b0300ecd7d57619e17f4937 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:53:11 +0530 Subject: [PATCH 3/5] feat: extend `Base.parentmodule` for `Model` --- src/systems/model_parsing.jl | 2 ++ test/model_parsing.jl | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 646aa9b888..7409601a9e 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -24,6 +24,8 @@ struct Model{F, S} end (m::Model)(args...; kw...) = m.f(args...; kw...) +Base.parentmodule(m::Model) = parentmodule(m.f) + for f in (:connector, :mtkmodel) isconnector = f == :connector ? true : false @eval begin diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 851aa33aab..3d6da78eab 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -688,3 +688,15 @@ end @test getdefault(component.written_out_for_1.sc) == 2 @test getdefault(component.written_out_for_2.sc) == 3 end + +module GetParentModule + +using ModelingToolkit + +@mtkmodel Component begin end + +end + +@testset "Parent module of Models" begin + @test parentmodule(GetParentModule.Component) == GetParentModule +end From a4a79ce18e45d1b4b6762c4a7cf5f7151d3cb8a2 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:16:58 +0530 Subject: [PATCH 4/5] test: use MyMockModule for testing parent-module --- test/model_parsing.jl | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 3d6da78eab..dafe46d388 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -689,14 +689,6 @@ end @test getdefault(component.written_out_for_2.sc) == 3 end -module GetParentModule - -using ModelingToolkit - -@mtkmodel Component begin end - -end - @testset "Parent module of Models" begin - @test parentmodule(GetParentModule.Component) == GetParentModule + @test parentmodule(MyMockModule.Ground) == MyMockModule end From e32463355a90ee09d6270515fe6dcf2392d1098b Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:20:31 +0530 Subject: [PATCH 5/5] test: conditionally define component array --- docs/src/basics/MTKModel_Connector.md | 2 +- src/systems/model_parsing.jl | 4 ++-- test/model_parsing.jl | 29 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/src/basics/MTKModel_Connector.md b/docs/src/basics/MTKModel_Connector.md index ef17518ff4..d4b81e32ff 100644 --- a/docs/src/basics/MTKModel_Connector.md +++ b/docs/src/basics/MTKModel_Connector.md @@ -230,7 +230,7 @@ end ``` !!! note - + For more examples of usage, checkout [ModelingToolkitStandardLibrary.jl](https://github.com/SciML/ModelingToolkitStandardLibrary.jl/) ## More on `Model.structure` diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 7409601a9e..7ee74e5dc1 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -844,9 +844,9 @@ function _parse_components!(body, kwargs) arg.args[2] = b expr = :(@named $arg) push!(comp_names, a) - if (isa(b.args[1], Symbol) || Meta.isexpr(b.args[1], :.)) + if (isa(b.args[1], Symbol) || Meta.isexpr(b.args[1], :.)) push!(comps, [a, b.args[1]]) - end + end end _ => error("Couldn't parse the component body: $arg") end diff --git a/test/model_parsing.jl b/test/model_parsing.jl index dafe46d388..ae3ec193b3 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -680,13 +680,40 @@ end :comprehension_2, :written_out_for_1, :written_out_for_2, - :single_sub_component, + :single_sub_component ] @test getdefault(component.comprehension_1.sc) == 1 @test getdefault(component.comprehension_2.sc) == 2 @test getdefault(component.written_out_for_1.sc) == 2 @test getdefault(component.written_out_for_2.sc) == 3 + + @mtkmodel ConditionalComponent begin + @structural_parameters begin + N = 2 + end + @components begin + if N == 2 + if_comprehension = [SubComponent(sc = i) for i in 1:N] + elseif N == 3 + elseif_comprehension = [SubComponent(sc = i) for i in 1:N] + else + else_comprehension = [SubComponent(sc = i) for i in 1:N] + end + end + end + + @named if_component = ConditionalComponent() + @test nameof.(get_systems(if_component)) == [:if_comprehension_1, :if_comprehension_2] + + @named elseif_component = ConditionalComponent(; N = 3) + @test nameof.(get_systems(elseif_component)) == + [:elseif_comprehension_1, :elseif_comprehension_2, :elseif_comprehension_3] + + @named else_component = ConditionalComponent(; N = 4) + @test nameof.(get_systems(else_component)) == + [:else_comprehension_1, :else_comprehension_2, + :else_comprehension_3, :else_comprehension_4] end @testset "Parent module of Models" begin