Skip to content

Commit 39d8f09

Browse files
committed
add functions to handle inputs and outputs
boundedness only affected by outer connections, not inner change inputs outputs function defs to short form more robust handling of observed outputs
1 parent 72a3978 commit 39d8f09

File tree

5 files changed

+248
-0
lines changed

5 files changed

+248
-0
lines changed

src/ModelingToolkit.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ using .BipartiteGraphs
112112

113113
include("variables.jl")
114114
include("parameters.jl")
115+
include("inputoutput.jl")
115116

116117
include("utils.jl")
117118
include("domains.jl")

src/inputoutput.jl

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using Symbolics: get_variables
2+
"""
3+
inputs(sys)
4+
5+
Return all variables that mare marked as inputs. See also [`unbound_inputs`](@ref)
6+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref)
7+
"""
8+
inputs(sys) = filter(isinput, states(sys))
9+
10+
"""
11+
outputs(sys)
12+
13+
Return all variables that mare marked as outputs. See also [`unbound_outputs`](@ref)
14+
See also [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
15+
"""
16+
function outputs(sys)
17+
o = observed(sys)
18+
rhss = [eq.rhs for eq in o]
19+
lhss = [eq.lhs for eq in o]
20+
unique([
21+
filter(isoutput, states(sys))
22+
filter(x -> x isa Term && isoutput(x), rhss) # observed can return equations with complicated expressions, we are only looking for single Terms
23+
filter(x -> x isa Term && isoutput(x), lhss)
24+
])
25+
end
26+
27+
"""
28+
bound_inputs(sys)
29+
30+
Return inputs that are bound within the system, i.e., internal inputs
31+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
32+
"""
33+
bound_inputs(sys) = filter(x->is_bound(sys, x), inputs(sys))
34+
35+
"""
36+
unbound_inputs(sys)
37+
38+
Return inputs that are not bound within the system, i.e., external inputs
39+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
40+
"""
41+
unbound_inputs(sys) = filter(x->!is_bound(sys, x), inputs(sys))
42+
43+
"""
44+
bound_outputs(sys)
45+
46+
Return outputs that are bound within the system, i.e., internal outputs
47+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
48+
"""
49+
bound_outputs(sys) = filter(x->is_bound(sys, x), outputs(sys))
50+
51+
"""
52+
unbound_outputs(sys)
53+
54+
Return outputs that are not bound within the system, i.e., external outputs
55+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
56+
"""
57+
unbound_outputs(sys) = filter(x->!is_bound(sys, x), outputs(sys))
58+
59+
"""
60+
is_bound(sys, u)
61+
62+
Determine whether or not input/output variable `u` is "bound" within the system, i.e., if it's to be considered internal to `sys`.
63+
A variable/signal is considered bound if it appears in an equation together with variables from other subsystems.
64+
The typical usecase for this function is to determine whether the input to an IO component is connected to another component,
65+
or if it remains an external input that the user has to supply before simulating the system.
66+
67+
See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref)
68+
"""
69+
function is_bound(sys, u, stack=[])
70+
#=
71+
For observed quantities, we check if a variable is connected to something that is bound to something further out.
72+
In the following scenario
73+
julia> observed(syss)
74+
2-element Vector{Equation}:
75+
sys₊y(tv) ~ sys₊x(tv)
76+
y(tv) ~ sys₊x(tv)
77+
sys₊y(t) is bound to the outer y(t) through the variable sys₊x(t) and should thus return is_bound(sys₊y(t)) = true.
78+
When asking is_bound(sys₊y(t)), we know that we are looking through observed equations and can thus ask
79+
if var is bound, if it is, then sys₊y(t) is also bound. This can lead to an infinite recursion, so we maintain a stack of variables we have previously asked about to be able to break cycles
80+
=#
81+
u Set(stack) && return false # Cycle detected
82+
eqs = equations(sys)
83+
eqs = filter(eq->has_var(eq, u), eqs) # Only look at equations that contain u
84+
# isout = isoutput(u)
85+
for eq in eqs
86+
vars = [get_variables(eq.rhs); get_variables(eq.lhs)]
87+
for var in vars
88+
var === u && continue
89+
if !same_or_inner_namespace(u, var)
90+
return true
91+
end
92+
end
93+
end
94+
# Look through observed equations as well
95+
oeqs = observed(sys)
96+
oeqs = filter(eq->has_var(eq, u), oeqs) # Only look at equations that contain u
97+
for eq in oeqs
98+
vars = [get_variables(eq.rhs); get_variables(eq.lhs)]
99+
for var in vars
100+
var === u && continue
101+
if !same_or_inner_namespace(u, var)
102+
return true
103+
end
104+
if is_bound(sys, var, [stack; u]) && !inner_namespace(u, var) # The variable we are comparing to can not come from an inner namespace, binding only counts outwards
105+
return true
106+
end
107+
end
108+
end
109+
false
110+
end
111+
112+
113+
114+
"""
115+
same_or_inner_namespace(u, var)
116+
117+
Determine whether or not `var` is in the same namespace as `u`, or a namespace internal to the namespace of `u`.
118+
Example: `sys.u ~ sys.inner.u` will bind `sys.inner.u`, but `sys.u` remains an unbound, external signal. The namepsaced signal `sys.inner.u` lives in a namspace internal to `sys`.
119+
"""
120+
function same_or_inner_namespace(u, var)
121+
nu = get_namespace(u)
122+
nv = get_namespace(var)
123+
nu == nv || # namespaces are the same
124+
startswith(nv, nu) || # or nv starts with nu, i.e., nv is an inner namepsace to nu
125+
occursin('', var) && !occursin('', u) # or u is top level but var is internal
126+
end
127+
128+
function inner_namespace(u, var)
129+
nu = get_namespace(u)
130+
nv = get_namespace(var)
131+
nu == nv && return false
132+
startswith(nv, nu) || # or nv starts with nu, i.e., nv is an inner namepsace to nu
133+
occursin('', var) && !occursin('', u) # or u is top level but var is internal
134+
end
135+
136+
"""
137+
get_namespace(x)
138+
139+
Return the namespace of a variable as a string. If the variable is not namespaced, the string is empty.
140+
"""
141+
function get_namespace(x)
142+
sname = string(x)
143+
parts = split(sname, '')
144+
if length(parts) == 1
145+
return ""
146+
end
147+
join(parts[1:end-1], '')
148+
end
149+
150+
"""
151+
has_var(eq, x)
152+
153+
Determine whether or not an equation or expression contains variable `x`.
154+
"""
155+
function has_var(eq::Equation, x)
156+
has_var(eq.rhs, x) || has_var(eq.lhs, x)
157+
end
158+
159+
has_var(ex, x) = x Set(get_variables(ex))
160+

src/variables.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct Equality <: AbstractConnectType end # Equality connection
1616
struct Flow <: AbstractConnectType end # sum to 0
1717
struct Stream <: AbstractConnectType end # special stream connector
1818

19+
isvarkind(m, x::Num) = isvarkind(m, value(x))
1920
function isvarkind(m, x)
2021
p = getparent(x, nothing)
2122
p === nothing || (x = p)

test/input_output_handling.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using ModelingToolkit, Symbolics, Test
2+
using ModelingToolkit: get_namespace, has_var, inputs, outputs, is_bound, bound_inputs, unbound_inputs, bound_outputs, unbound_outputs, isinput, isoutput
3+
4+
5+
# Test input handling
6+
@parameters tv
7+
D = Differential(tv)
8+
@variables x(tv) u(tv) [input=true]
9+
@test isinput(u)
10+
11+
@named sys = ODESystem([D(x) ~ -x + u], tv) # both u and x are unbound
12+
@named sys2 = ODESystem([D(x) ~ -sys.x], tv, systems=[sys]) # this binds sys.x in the context of sys2, sys2.x is still unbound
13+
@named sys3 = ODESystem([D(x) ~ -sys.x + sys.u], tv, systems=[sys]) # This binds both sys.x and sys.u
14+
15+
@named sys4 = ODESystem([D(x) ~ -sys.x, u~sys.u], tv, systems=[sys]) # This binds both sys.x and sys3.u, this system is one layer deeper than the previous. u is directly forwarded to sys.u, and in this case sys.u is bound while u is not
16+
17+
@test has_var(x ~ 1, x)
18+
@test has_var(1 ~ x, x)
19+
@test has_var(x + x, x)
20+
@test !has_var(2 ~ 1, x)
21+
22+
@test get_namespace(x) == ""
23+
@test get_namespace(sys.x) == "sys"
24+
@test get_namespace(sys2.x) == "sys2"
25+
@test get_namespace(sys2.sys.x) == "sys2₊sys"
26+
27+
@test !is_bound(sys, u)
28+
@test !is_bound(sys, x)
29+
@test !is_bound(sys, sys.u)
30+
@test is_bound(sys2, sys.x)
31+
@test !is_bound(sys2, sys.u)
32+
@test !is_bound(sys2, sys2.sys.u)
33+
34+
@test is_bound(sys3, sys.u) # I would like to write sys3.sys.u here but that's not how the variable is stored in the equations
35+
@test is_bound(sys3, sys.x)
36+
37+
@test is_bound(sys4, sys.u)
38+
@test !is_bound(sys4, u)
39+
40+
@test isequal(inputs(sys), [u])
41+
@test isequal(inputs(sys2), [sys.u])
42+
43+
@test isempty(bound_inputs(sys))
44+
@test isequal(unbound_inputs(sys), [u])
45+
46+
@test isempty(bound_inputs(sys2))
47+
@test isequal(unbound_inputs(sys2), [sys.u])
48+
49+
@test isequal(bound_inputs(sys3), [sys.u])
50+
@test isempty(unbound_inputs(sys3))
51+
52+
53+
54+
# Test output handling
55+
@parameters tv
56+
D = Differential(tv)
57+
@variables x(tv) y(tv) [output=true]
58+
@test isoutput(y)
59+
@named sys = ODESystem([D(x) ~ -x, y ~ x], tv) # both y and x are unbound
60+
syss = structural_simplify(sys) # This makes y an observed variable
61+
62+
@named sys2 = ODESystem([D(x) ~ -sys.x, y~sys.y], tv, systems=[sys])
63+
64+
@test !is_bound(sys, y)
65+
@test !is_bound(sys, x)
66+
@test !is_bound(sys, sys.y)
67+
68+
@test !is_bound(syss, y)
69+
@test !is_bound(syss, x)
70+
@test !is_bound(syss, sys.y)
71+
72+
@test isequal(unbound_outputs(sys), [y])
73+
@test isequal(unbound_outputs(syss), [y])
74+
75+
@test isequal(unbound_outputs(sys2), [y])
76+
@test isequal(bound_outputs(sys2), [sys.y])
77+
78+
syss = structural_simplify(sys2)
79+
80+
@test !is_bound(syss, y)
81+
@test !is_bound(syss, x)
82+
@test is_bound(syss, sys.y)
83+
84+
@test isequal(unbound_outputs(syss), [y])
85+
@test isequal(bound_outputs(syss), [sys.y])

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ using SafeTestsets, Test
77
@safetestset "Simplify Test" begin include("simplify.jl") end
88
@safetestset "Direct Usage Test" begin include("direct.jl") end
99
@safetestset "System Linearity Test" begin include("linearity.jl") end
10+
@safetestset "Input Output Test" begin include("input_output_handling.jl") end
1011
@safetestset "DiscreteSystem Test" begin include("discretesystem.jl") end
1112
@safetestset "ODESystem Test" begin include("odesystem.jl") end
1213
@safetestset "Unitful Quantities Test" begin include("units.jl") end

0 commit comments

Comments
 (0)