diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 4a7909e41e..59ad7eefb4 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -36,6 +36,7 @@ else const IntDisjointSet = IntDisjointSets end using Base.Threads +using Base.ScopedValues using Latexify, Unitful, ArrayInterface using Setfield, ConstructionBase import Libdl diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 476996b3e8..d64f0e2776 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2153,7 +2153,10 @@ function _named(name, call, runtime = false) op = call.args[1] quote $is_sys_construction = ($op isa $DataType) && ($op <: $AbstractSystem) - $call + # It is counterintuitive that `@accept_unnamed_model` is used inside of `@named`, but + # the point is that inside of `@named my_model = Model(sub_component=MyComponent())`, + # `sub_component` does not require an explicit `name`. + $ModelingToolkit.@accept_unnamed_model $call end end diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 699cfee8fd..6392b1ca25 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -1,3 +1,16 @@ +const accept_unnamed_model = ScopedValue(false) +macro accept_unnamed_model(expr) + esc(:($with(()->$expr, $ModelingToolkit.accept_unnamed_model => true))) +end +function default_model_name(m) + if accept_unnamed_model[] + return :unnamed + else + error("Model constructor ", m, " require a `name=` keyword argument ", + "(or usage of `@named`, `@mtkcompile`)") + end +end + """ $(TYPEDEF) @@ -22,7 +35,7 @@ struct Model{F, S} """ isconnector::Bool end -(m::Model)(args...; kw...) = m.f(args...; kw...) +(m::Model)(args...; name=default_model_name(m), kw...) = m.f(args...; name, kw...) Base.parentmodule(m::Model) = parentmodule(m.f) @@ -41,6 +54,24 @@ function flatten_equations(eqs::Vector{Union{Equation, Vector{Equation}}}) foldl(flatten_equations, eqs; init = Equation[]) end +""" This `ScopedValue` is used to handle structural parameter and component change via +`outer__inner__variable`-type kwargs. The idea is that while we can't easily thread these kwargs +from `outer` to `inner` explicitly, we can pass them on via `passed_kwargs`. """ +const passed_kwargs = ScopedValue(Dict{Symbol, Any}()) +lookup_passed_kwarg(kwarg::Symbol, val) = get(passed_kwargs[], kwarg, val) +lookup_passed_kwarg(num::Num, val) = lookup_passed_kwarg(Symbol(string(num)), val) + +setdefault_g(p::Num, val) = setdefault(p, lookup_passed_kwarg(Symbol(string(p)), val)) +setdefault_g(p, val) = setdefault(p, val) # fallback, sometimes p is some exotic structure + +function construct_subcomponent(body::Function, lhs, other_kwargs) + root = string(lhs, "__") + dict2 = Dict{Symbol, Any}(Symbol(string(k)[length(root)+1:end])=>v + for (k, v) in merge(passed_kwargs[], other_kwargs) + if startswith(string(k), root)) + return with(body, passed_kwargs => dict2) +end + function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) if fullname isa Symbol name, type = fullname, :System @@ -156,11 +187,11 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) push!(exprs.args, :(var"#___sys___")) f = if length(where_types) == 0 - :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) + :($(Symbol(:__, name, :__))(; name, $(kwargs...), other_kwargs...) = $exprs) else f_with_where = Expr(:where) push!(f_with_where.args, - :($(Symbol(:__, name, :__))(; name, $(kwargs...))), where_types...) + :($(Symbol(:__, name, :__))(; name, $(kwargs...), other_kwargs...)), where_types...) :($f_with_where = $exprs) end @@ -728,6 +759,7 @@ function parse_structural_parameters!(exprs, sps, dict, mod, body, kwargs) b = _type_check!(get_var(mod, b), a, type, :structural_parameters) push!(sps, a) push!(kwargs, Expr(:kw, Expr(:(::), a, type), b)) + push!(exprs, :($a = $lookup_passed_kwarg($(Expr(:quote, a)), $a))) dict[:structural_parameters][a] = dict[:kwargs][a] = Dict( :value => b, :type => type) end @@ -736,11 +768,12 @@ function parse_structural_parameters!(exprs, sps, dict, mod, body, kwargs) b) => begin push!(sps, a) push!(kwargs, Expr(:kw, a, b)) + push!(exprs, :($a = $lookup_passed_kwarg($(Expr(:quote, a)), $a))) dict[:structural_parameters][a] = dict[:kwargs][a] = Dict(:value => b) end a => begin push!(sps, a) - push!(kwargs, a) + push!(kwargs, a) # TODO: maybe use NOVALUE dict[:structural_parameters][a] = dict[:kwargs][a] = Dict(:value => nothing) end end @@ -925,10 +958,10 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) unit = metadata_with_exprs[VariableUnit] quote $name = if $name === $NO_VALUE - $setdefault($vv, $def) + $setdefault($vv, $lookup_passed_kwarg($vv, $def)) else try - $setdefault($vv, $convert_units($unit, $name)) + $setdefault($vv, $convert_units($unit, $lookup_passed_kwarg($vv, $name))) catch e if isa(e, $(DynamicQuantities.DimensionError)) || isa(e, $(Unitful.DimensionError)) @@ -945,9 +978,9 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) else quote $name = if $name === $NO_VALUE - $setdefault($vv, $def) + $setdefault_g($vv, $def) else - $setdefault($vv, $name) + $setdefault_g($vv, $name) end end end @@ -1283,192 +1316,62 @@ end ### Parsing Components: -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 - for i in start:lastindex(b.args) - arg = b.args[i] - arg isa LineNumberNode && continue - MLStyle.@match arg begin - x::Symbol || - Expr(:kw, x) => begin - 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, varexpr, kwargs) - end - Expr(:kw, - x, - y) => begin - 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 +""" `push!`, or `append!`, depending on `x`'s type """ +push_append!(vec, x::AbstractVector) = append!(vec, x) +push_append!(vec, x) = push!(vec, x) -model_name(name, range) = Symbol.(name, :_, collect(range)) +""" Helper; renames a single component or a vector of component. """ +component_rename(obj, name::Symbol) = rename(obj, name) +component_rename(objs::Vector, name::Symbol) = + [rename(obj, Symbol(name, :_, i)) for (i, obj) in enumerate(objs)] -function _parse_components!(body, kwargs) - local expr - varexpr = Expr(:block) - comps = Vector{Union{Union{Expr, Symbol}, Expr}}[] - comp_names = [] - - Base.remove_linenums!(body) - arg = body.args[end] +""" Recursively parse an expression inside of the `@components` block. - MLStyle.@match arg begin - Expr(:(=), - a, - Expr(:comprehension, Expr(:generator, b, Expr(:(=), c, d)))) => begin - array_varexpr = Expr(:block) - - push!(comp_names, :($a...)) - push!(comps, [a, b.args[1], d]) - b = deepcopy(b) - - 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") +Recursive is to handle nested `if` blocks. """ +function parse_components_expr!(exprs, cs, dict, compexpr, kwargs) + MLStyle.@match compexpr begin + Expr(:block, args...) => begin + for a in args + parse_components_expr!(exprs, cs, dict, a, kwargs) + end end - Expr(:block) => begin - # TODO: Do we need this? - error("Multiple `@components` block detected within a single block") + Expr(:if, condition, x) => begin + then_exprs = [] + parse_components_expr!(then_exprs, cs, dict, x, kwargs) + push!(exprs, :(if $condition begin $(then_exprs...) end end)) 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) + Expr(:if, condition, x, y) => begin + then_exprs = [] + else_exprs = [] + parse_components_expr!(then_exprs, cs, dict, x, kwargs) + parse_components_expr!(else_exprs, cs, dict, y, kwargs) + push!(exprs, :(if $condition + begin $(then_exprs...) end + else + begin $(else_exprs...) end + end)) 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]]) - end + Expr(:elseif, args...) => parse_components_expr!(exprs, cs, dict, Expr(:if, args...), kwargs) + Expr(:(=), lhs, rhs) => begin + push!(dict[:components], [lhs, :unimplemented]) # TODO + rhs_2 = :($construct_subcomponent($(Expr(:quote, lhs)), other_kwargs) do; $rhs end) + push!(exprs, + # The logic here is a bit complicated; the component can be taken from either + # the kwargs, or `passed_kwargs`. TODO: simplify + :($lhs = $component_rename($lookup_passed_kwarg($(Expr(:quote, lhs)), + $Base.@something($lhs, $rhs_2)), + $(Expr(:quote, lhs))))) + push!(exprs, :($push_append!(systems, $lhs))) + push!(kwargs, Expr(:kw, lhs, :nothing)) end - _ => error("Couldn't parse the component body: $arg") + _ => error("Expression not handled ", compexpr) 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, 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!(x, kwargs) - push_conditional_component!(ifexpr, expr_vec, comp_names, varexpr) - comps -end - -function handle_if_y!(exprs, ifexpr, y, kwargs) - Base.remove_linenums!(y) - if Meta.isexpr(y, :elseif) - comps = [:elseif, y.args[1]] - elseifexpr = Expr(:elseif) - push!(comps, handle_if_x!(mod, exprs, elseifexpr, y.args[2], kwargs, y.args[1])) - get(y.args, 3, nothing) !== nothing && - push!(comps, handle_if_y!(exprs, elseifexpr, y.args[3], kwargs)) - push!(ifexpr.args, elseifexpr) - (comps...,) - else - comp_names, comps, expr_vec, varexpr = _parse_components!(y, kwargs) - push_conditional_component!(ifexpr, expr_vec, comp_names, varexpr) - comps - end -end - -function handle_conditional_components(condition, dict, exprs, kwargs, x, y = nothing) - ifexpr = Expr(:if) - comps = handle_if_x!(mod, exprs, ifexpr, x, kwargs, condition) - ycomps = y === nothing ? [] : handle_if_y!(exprs, ifexpr, y, kwargs) - push!(exprs, ifexpr) - push!(dict[:components], (:if, condition, comps, ycomps)) end function parse_components!(exprs, cs, dict, compbody, kwargs) dict[:components] = [] Base.remove_linenums!(compbody) - for arg in compbody.args - MLStyle.@match arg begin - Expr(:if, condition, - x) => begin - handle_conditional_components(condition, dict, exprs, kwargs, x) - end - Expr(:if, - condition, - x, - y) => begin - handle_conditional_components(condition, dict, exprs, kwargs, x, y) - end - # 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, expr_vec) - end - end - end + parse_components_expr!(exprs, cs, dict, compbody, kwargs) end function _rename(compname, varname) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 6a3e29fda4..02e1fb8336 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -172,7 +172,7 @@ resistor = getproperty(rc, :resistor; namespace = false) @test getdefault(rc.capacitor.v) == 0.0 @test get_gui_metadata(rc.resistor).layout == Resistor.structure[:icon] == - read(joinpath(ENV["MTK_ICONS_DIR"], "resistor.svg"), String) + read(joinpath(ENV["MTK_ICONS_DIR"], "resistor.svg"), String) @test get_gui_metadata(rc.ground).layout == read(abspath(ENV["MTK_ICONS_DIR"], "ground.svg"), String) @test get_gui_metadata(rc.capacitor).layout == @@ -464,7 +464,7 @@ end @test A.structure[:kwargs] == Dict{Symbol, Dict}( :p => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), :v => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)) - @test A.structure[:components] == [[:cc, :C]] + @test_broken A.structure[:components] == [[:cc, :C]] end using ModelingToolkit: D_nounits @@ -529,6 +529,7 @@ end @mtkmodel InsideTheBlock begin @structural_parameters begin flag = 1 + inner_flag = 1 end @parameters begin eq = flag == 1 ? 1 : 0 @@ -543,7 +544,11 @@ end @components begin default_sys = C() if flag == 1 - if_sys = C() + if inner_flag == 1 + if_if_sys = C() + else + if_else_sys = C() + end elseif flag == 2 elseif_sys = C() else @@ -565,6 +570,8 @@ end @named if_in_sys = InsideTheBlock() if_in_sys = complete(if_in_sys; flatten = false) + @named if_else_in_sys = InsideTheBlock(inner_flag = 2) + if_else_in_sys = complete(if_else_in_sys; flatten = false) @named elseif_in_sys = InsideTheBlock(flag = 2) elseif_in_sys = complete(elseif_in_sys; flatten = false) @named else_in_sys = InsideTheBlock(flag = 3) @@ -578,9 +585,10 @@ end @test getdefault(elseif_in_sys.elseif_parameter) == 101 @test getdefault(else_in_sys.else_parameter) == 102 - @test nameof.(get_systems(if_in_sys)) == [:if_sys, :default_sys] - @test nameof.(get_systems(elseif_in_sys)) == [:elseif_sys, :default_sys] - @test nameof.(get_systems(else_in_sys)) == [:else_sys, :default_sys] + @test nameof.(get_systems(if_in_sys)) == [ :default_sys, :if_if_sys] + @test nameof.(get_systems(if_else_in_sys)) == [ :default_sys, :if_else_sys] + @test nameof.(get_systems(elseif_in_sys)) == [:default_sys, :elseif_sys] + @test nameof.(get_systems(else_in_sys)) == [:default_sys, :else_sys] @test all([ if_in_sys.eq ~ 0, @@ -671,9 +679,9 @@ end @test getdefault(elseif_out_sys.elseif_parameter) == 101 @test getdefault(else_out_sys.else_parameter) == 102 - @test nameof.(get_systems(if_out_sys)) == [:if_sys, :default_sys] - @test nameof.(get_systems(elseif_out_sys)) == [:elseif_sys, :default_sys] - @test nameof.(get_systems(else_out_sys)) == [:else_sys, :default_sys] + @test nameof.(get_systems(if_out_sys)) == [:default_sys, :if_sys] + @test nameof.(get_systems(elseif_out_sys)) == [:default_sys, :elseif_sys ] + @test nameof.(get_systems(else_out_sys)) == [:default_sys, :else_sys] @test Equation[if_out_sys.if_parameter ~ 0 if_out_sys.default_parameter ~ 0] == equations(if_out_sys) @@ -745,17 +753,24 @@ end end end + @mtkmodel AlternativeComponent begin + @parameters begin + ac + 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 + written_out_for = map(1:N) do i sc = i + 1 SubComponent(; sc) end single_sub_component = SubComponent() + heterogeneous_components = [SubComponent(sc=1), AlternativeComponent(ac=3)] end end @@ -767,13 +782,17 @@ end :comprehension_2, :written_out_for_1, :written_out_for_2, - :single_sub_component + :single_sub_component, + :heterogeneous_components_1, + :heterogeneous_components_2 ] @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 + @test getdefault(component.heterogeneous_components_1.sc) == 1 + @test getdefault(component.heterogeneous_components_2.ac) == 3 @mtkmodel ConditionalComponent begin @structural_parameters begin @@ -1065,16 +1084,16 @@ end 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 @@ -1090,88 +1109,124 @@ end @test ModelingToolkit.getmetadata(test_model, NewInt, nothing) === 1 end -@testset "Pass parameters of higher level models as structural parameters" begin - let D=ModelingToolkit.D_nounits, t=ModelingToolkit.t_nounits - """ - ╭─────────╮ - in │ K │ out - ╶─>─┤ ------- ├──>─╴ - │ 1 + s T │ - ╰─────────╯ - """ - @mtkmodel SimpleLag begin - @structural_parameters begin - K # Gain - T # Time constant - end - @variables begin - in(t), [description="Input signal", input=true] - out(t), [description="Output signal", output=true] - end - @equations begin - T * D(out) ~ K*in - out - end +# @testset "Pass parameters of higher level models as structural parameters" begin +# let D=ModelingToolkit.D_nounits, t=ModelingToolkit.t_nounits +# """ +# ╭─────────╮ +# in │ K │ out +# ╶─>─┤ ------- ├──>─╴ +# │ 1 + s T │ +# ╰─────────╯ +# """ +# @mtkmodel SimpleLag begin +# @structural_parameters begin +# K # Gain +# T # Time constant +# end +# @variables begin +# in(t), [description="Input signal", input=true] +# out(t), [description="Output signal", output=true] +# end +# @equations begin +# T * D(out) ~ K*in - out +# end +# end + +# """ +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ DoubleLag ┃ +# ┃ ╭─────────╮ ╭─────────╮ ┃ +# in ┃ │ K1 │ │ K2 │ ┃ out +# ─>──╂─┤ ------- ├──┤ ------- ├─╂──>──╴ +# ┃ │ 1 + sT1 │ │ 1 + sT2 │ ┃ +# ┃ ╰─────────╯ ╰─────────╯ ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +# """ +# @mtkmodel DoubleLag begin +# @parameters begin +# K1, [description="Proportional gain 1"] +# T1, [description="Time constant 1"] +# K2, [description="Proportional gain 2"] +# T2, [description="Time constant 2"] +# end +# @components begin +# lag1 = SimpleLag(K = K1, T = T1) +# lag2 = SimpleLag(K = K2, T = T2) +# end +# @variables begin +# in(t), [description="Input signal", input=true] +# out(t), [description="Output signal", output=true] +# end +# @equations begin +# in ~ lag1.in +# lag1.out ~ lag2.in +# out ~ lag2.out +# end +# end + +# @mtkmodel ClosedSystem begin +# @components begin +# double_lag = DoubleLag(; K1 = 1, K2 = 2, T1 = 0.1, T2 = 0.2) +# end +# @equations begin +# double_lag.in ~ 1.0 +# end +# end + +# @mtkbuild sys = ClosedSystem() +# @test length(parameters(sys)) == 4 +# @test length(unknowns(sys)) == 2 + +# p = MTKParameters(sys, defaults(sys)) +# u = [0.5 for i in 1:2] +# du = zeros(2) +# # update du for given u and p +# ODEFunction(sys).f.f_iip(du, u, p, 0.0) + +# # find indices of lag1 and lag2 states (might be reordered due to simplification details) +# symnames = string.(ModelingToolkit.getname.(variable_symbols(sys))) +# lag1idx = findall(contains("1"), symnames) |> only +# lag2idx = findall(contains("2"), symnames) |> only + +# # check du values +# K1, K2, T1, T2 = 1, 2, 0.1, 0.2 +# @test du[lag1idx] ≈ (K1*1.0 - u[lag1idx]) / T1 +# @test du[lag2idx] ≈ (K2*u[lag1idx] - u[lag2idx]) / T2 +# end +# end + +@testset "__ kwarg handling" begin + @mtkmodel Inner begin + @structural_parameters begin + N = 2 end + @parameters begin + p1[1:N] = fill(3, N) + end + end - """ - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ DoubleLag ┃ - ┃ ╭─────────╮ ╭─────────╮ ┃ - in ┃ │ K1 │ │ K2 │ ┃ out - ─>──╂─┤ ------- ├──┤ ------- ├─╂──>──╴ - ┃ │ 1 + sT1 │ │ 1 + sT2 │ ┃ - ┃ ╰─────────╯ ╰─────────╯ ┃ - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - """ - @mtkmodel DoubleLag begin - @parameters begin - K1, [description="Proportional gain 1"] - T1, [description="Time constant 1"] - K2, [description="Proportional gain 2"] - T2, [description="Time constant 2"] - end - @components begin - lag1 = SimpleLag(K = K1, T = T1) - lag2 = SimpleLag(K = K2, T = T2) - end - @variables begin - in(t), [description="Input signal", input=true] - out(t), [description="Output signal", output=true] - end - @equations begin - in ~ lag1.in - lag1.out ~ lag2.in - out ~ lag2.out - end + @mtkmodel Outer begin + @components begin + inner = Inner() end + end - @mtkmodel ClosedSystem begin - @components begin - double_lag = DoubleLag(; K1 = 1, K2 = 2, T1 = 0.1, T2 = 0.2) - end - @equations begin - double_lag.in ~ 1.0 - end + @mtkmodel World begin + @components begin + outer = Outer() end + end + + # Strutural parameter change + @named world2 = World(outer__inner__N=5) + @test ModelingToolkit.defaults(world2)[@nonamespace(world2.outer).inner.p1[5]] == 3 - @mtkbuild sys = ClosedSystem() - @test length(parameters(sys)) == 4 - @test length(unknowns(sys)) == 2 - - p = MTKParameters(sys, defaults(sys)) - u = [0.5 for i in 1:2] - du = zeros(2) - # update du for given u and p - ODEFunction(sys).f.f_iip(du, u, p, 0.0) - - # find indices of lag1 and lag2 states (might be reordered due to simplification details) - symnames = string.(ModelingToolkit.getname.(variable_symbols(sys))) - lag1idx = findall(contains("1"), symnames) |> only - lag2idx = findall(contains("2"), symnames) |> only - - # check du values - K1, K2, T1, T2 = 1, 2, 0.1, 0.2 - @test du[lag1idx] ≈ (K1*1.0 - u[lag1idx]) / T1 - @test du[lag2idx] ≈ (K2*u[lag1idx] - u[lag2idx]) / T2 + # Component replacement + @mtkmodel AlternativeInner begin + @parameters begin + alt_level = 1 + end end + @named world = World(outer__inner=AlternativeInner(alt_level=10)) + @test ModelingToolkit.defaults(world)[@nonamespace(world.outer).inner.alt_level] == 10 end