Skip to content

Commit 7c7b98d

Browse files
baggepinnenFredrik Bagge Carlson
andauthored
add variable metadata (#1560)
* add variable metadata closes #1509 * optional default tunable metadata Co-authored-by: Fredrik Bagge Carlson <[email protected]>
1 parent e0bdf29 commit 7c7b98d

File tree

6 files changed

+331
-0
lines changed

6 files changed

+331
-0
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ makedocs(
3030
"Basics" => Any[
3131
"basics/AbstractSystem.md",
3232
"basics/ContextualVariables.md",
33+
"basics/Variable_metadata.md",
3334
"basics/Composition.md",
3435
"basics/Validation.md",
3536
"basics/DependencyGraphs.md",

docs/src/basics/Variable_metadata.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Symbolic metadata
2+
It is possible to add metadata to symbolic variables. The following
3+
information can be added (note, it's possible to extend this to user-defined metadata as well)
4+
5+
## Input or output
6+
Designate a variable as either an input or an output using the following
7+
```@example metadata
8+
using ModelingToolkit
9+
@variables u [input=true]
10+
isinput(u)
11+
```
12+
```@example metadata
13+
@variables y [output=true]
14+
isoutput(y)
15+
```
16+
17+
## Bounds
18+
Bounds are useful when parameters are to be optimized, or to express intervals of uncertainty.
19+
20+
```@example metadata
21+
@variables u [bounds=(-1,1)]
22+
hasbounds(u)
23+
```
24+
```@example metadata
25+
getbounds(u)
26+
```
27+
28+
## Mark input as a disturbance
29+
Indicate that an input is not available for control, i.e., it's a disturbance input.
30+
31+
```@example metadata
32+
@variables u [input=true, disturbance=true]
33+
isdisturbance(u)
34+
```
35+
36+
## Mark parameter as tunable
37+
Indicate that a parameter can be automatically tuned by automatic control tuning apps.
38+
39+
```@example metadata
40+
@parameters Kp [tunable=true]
41+
istunable(Kp)
42+
```
43+
44+
## Probability distributions
45+
A probability distribution may be associated with a parameter to indicate either
46+
uncertainty about it's value, or as a prior distribution for Bayesian optimization.
47+
48+
```julia
49+
using Distributions
50+
d = Normal(10, 1)
51+
@parameters m [dist=d]
52+
hasdist(m)
53+
```
54+
```julia
55+
getdist(m)
56+
```
57+
58+
## Additional functions
59+
For systems that contain parameters with metadata like described above have some additional functions defined for convenience.
60+
In the example below, we define a system with tunable parameters and extract bounds vectors
61+
62+
```@example metadata
63+
@parameters t
64+
Dₜ = Differential(t)
65+
@variables x(t)=0 u(t)=0 [input=true] y(t)=0 [output=true]
66+
@parameters T [tunable = true, bounds = (0, Inf)]
67+
@parameters k [tunable = true, bounds = (0, Inf)]
68+
eqs = [
69+
Dₜ(x) ~ (-x + k*u) / T # A first-order system with time constant T and gain k
70+
y ~ x
71+
]
72+
sys = ODESystem(eqs, t, name=:tunable_first_order)
73+
```
74+
```@example metadata
75+
p = tunable_parameters(sys) # extract all parameters marked as tunable
76+
```
77+
```@example metadata
78+
lb, ub = getbounds(p) # operating on a vector, we get lower and upper bound vectors
79+
```
80+
```@example metadata
81+
b = getbounds(sys) # Operating on the system, we get a dict
82+
```
83+
84+
85+
## Index
86+
```@index
87+
Pages = ["Variable_metadata.md"]
88+
```
89+
90+
## Docstrings
91+
```@autodocs
92+
Modules = [ModelingToolkit]
93+
Pages = ["variables.jl"]
94+
Private = false
95+
```

src/ModelingToolkit.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export NonlinearSystem, OptimizationSystem
173173
export ControlSystem
174174
export alias_elimination, flatten
175175
export connect, @connector, Connection, Flow, Stream, instream
176+
export isinput, isoutput, getbounds, hasbounds, isdisturbance, istunable, getdist, hasdist, tunable_parameters
176177
export ode_order_lowering, liouville_transform
177178
export runge_kutta_discretize
178179
export PDESystem

src/variables.jl

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,167 @@ ishistory(x) = ishistory(unwrap(x))
9494
ishistory(x::Symbolic) = getmetadata(x, IsHistory, false)
9595
hist(x, t) = wrap(hist(unwrap(x), t))
9696
hist(x::Symbolic, t) = setmetadata(toparam(similarterm(x, operation(x), [unwrap(t)], metadata=metadata(x))), IsHistory, true)
97+
98+
99+
## Bounds ======================================================================
100+
struct VariableBounds end
101+
Symbolics.option_to_metadata_type(::Val{:bounds}) = VariableBounds
102+
getbounds(x::Num) = getbounds(Symbolics.unwrap(x))
103+
104+
"""
105+
getbounds(x)
106+
107+
Get the bounds associated with symbolc variable `x`.
108+
Create parameters with bounds like this
109+
```
110+
@parameters p [bounds=(-1, 1)]
111+
```
112+
"""
113+
function getbounds(x)
114+
p = Symbolics.getparent(x, nothing)
115+
p === nothing || (x = p)
116+
Symbolics.getmetadata(x, VariableBounds, (-Inf, Inf))
117+
end
118+
119+
"""
120+
hasbounds(x)
121+
122+
Determine whether or not symbolic variable `x` has bounds associated with it.
123+
See also [`getbounds`](@ref).
124+
"""
125+
function hasbounds(x)
126+
b = getbounds(x)
127+
isfinite(b[1]) && isfinite(b[2])
128+
end
129+
130+
131+
## Disturbance =================================================================
132+
struct VariableDisturbance end
133+
Symbolics.option_to_metadata_type(::Val{:disturbance}) = VariableDisturbance
134+
135+
isdisturbance(x::Num) = isdisturbance(Symbolics.unwrap(x))
136+
137+
"""
138+
isdisturbance(x)
139+
140+
Determine whether or not symbolic variable `x` is marked as a disturbance input.
141+
"""
142+
function isdisturbance(x)
143+
p = Symbolics.getparent(x, nothing)
144+
p === nothing || (x = p)
145+
Symbolics.getmetadata(x, VariableDisturbance, false)
146+
end
147+
148+
149+
## Tunable =====================================================================
150+
struct VariableTunable end
151+
Symbolics.option_to_metadata_type(::Val{:tunable}) = VariableTunable
152+
153+
istunable(x::Num, args...) = istunable(Symbolics.unwrap(x), args...)
154+
155+
"""
156+
istunable(x, default = false)
157+
158+
Determine whether or not symbolic variable `x` is marked as a tunable for an automatic tuning algorithm.
159+
160+
`default` indicates whether variables without `tunable` metadata are to be considered tunable or not.
161+
162+
Create a tunable parameter by
163+
```
164+
@parameters u [tunable=true]
165+
```
166+
See also [`tunable_parameters`](@ref), [`getbounds`](@ref)
167+
"""
168+
function istunable(x, default=false)
169+
p = Symbolics.getparent(x, nothing)
170+
p === nothing || (x = p)
171+
Symbolics.getmetadata(x, VariableTunable, default)
172+
end
173+
174+
175+
## Dist ========================================================================
176+
struct VariableDistribution end
177+
Symbolics.option_to_metadata_type(::Val{:dist}) = VariableDistribution
178+
getdist(x::Num) = getdist(Symbolics.unwrap(x))
179+
180+
"""
181+
getdist(x)
182+
183+
Get the probability distribution associated with symbolc variable `x`. If no distribution
184+
is associated with `x`, `nothing` is returned.
185+
Create parameters with associated distributions like this
186+
```julia
187+
using Distributions
188+
d = Normal(0, 1)
189+
@parameters u [dist=d]
190+
hasdist(u) # true
191+
getdist(u) # retrieve distribution
192+
```
193+
"""
194+
function getdist(x)
195+
p = Symbolics.getparent(x, nothing)
196+
p === nothing || (x = p)
197+
Symbolics.getmetadata(x, VariableDistribution, nothing)
198+
end
199+
200+
"""
201+
hasdist(x)
202+
203+
Determine whether or not symbolic variable `x` has a probability distribution associated with it.
204+
"""
205+
function hasdist(x)
206+
b = getdist(x)
207+
b !== nothing
208+
end
209+
210+
211+
## System interface
212+
213+
"""
214+
tunable_parameters(sys, p = parameters(sys); default=false)
215+
216+
Get all parameters of `sys` that are marked as `tunable`.
217+
218+
Keyword argument `default` indicates whether variables without `tunable` metadata are to be considered tunable or not.
219+
220+
Create a tunable parameter by
221+
```
222+
@parameters u [tunable=true]
223+
```
224+
See also [`getbounds`](@ref), [`istunable`](@ref)
225+
"""
226+
function tunable_parameters(sys, p = parameters(sys); default=false)
227+
filter(x->istunable(x, default), p)
228+
end
229+
230+
"""
231+
getbounds(sys::ModelingToolkit.AbstractSystem)
232+
233+
Returns a dict with pairs `p => (lb, ub)` mapping parameters of `sys` to lower and upper bounds.
234+
Create parameters with bounds like this
235+
```
236+
@parameters p [bounds=(-1, 1)]
237+
```
238+
"""
239+
function getbounds(sys::ModelingToolkit.AbstractSystem)
240+
p = parameters(sys)
241+
Dict(p .=> getbounds.(p))
242+
end
243+
244+
"""
245+
lb, ub = getbounds(p::AbstractVector)
246+
247+
Return vectors of lower and upper bounds of parameter vector `p`.
248+
Create parameters with bounds like this
249+
```
250+
@parameters p [bounds=(-1, 1)]
251+
```
252+
See also [`tunable_parameters`](@ref), [`hasbounds`](@ref)
253+
"""
254+
function getbounds(p::AbstractVector)
255+
bounds = getbounds.(p)
256+
lb = first.(bounds)
257+
ub = last.(bounds)
258+
(; lb, ub)
259+
end
260+

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ using SafeTestsets, Test
3030
@safetestset "Precompiled Modules Test" begin include("precompile_test.jl") end
3131
@testset "Distributed Test" begin include("distributed.jl") end
3232
@safetestset "Variable Utils Test" begin include("variable_utils.jl") end
33+
@safetestset "Variable Metadata Test" begin include("test_variable_metadata.jl") end
3334
@safetestset "DAE Jacobians Test" begin include("dae_jacobian.jl") end
3435
@safetestset "Jacobian Sparsity" begin include("jacobiansparsity.jl") end
3536
println("Last test requires gcc available in the path!")

test/test_variable_metadata.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using ModelingToolkit
2+
3+
# Bounds
4+
@variables u [bounds=(-1,1)]
5+
@test getbounds(u) == (-1, 1)
6+
@test hasbounds(u)
7+
8+
@variables y
9+
@test !hasbounds(y)
10+
11+
12+
# Disturbance
13+
@variables u [disturbance=true]
14+
@test isdisturbance(u)
15+
16+
@variables y
17+
@test !isdisturbance(y)
18+
19+
20+
# Tunable
21+
@parameters u [tunable=true]
22+
@test istunable(u)
23+
24+
@parameters y
25+
@test !istunable(y)
26+
27+
# Distributions
28+
struct FakeNormal end
29+
d = FakeNormal()
30+
@parameters u [dist=d]
31+
@test hasdist(u)
32+
@test getdist(u) == d
33+
34+
@parameters y
35+
@test !hasdist(y)
36+
37+
## System interface
38+
@parameters t
39+
Dₜ = Differential(t)
40+
@variables x(t)=0 u(t)=0 [input=true] y(t)=0 [output=true]
41+
@parameters T [tunable = true, bounds = (0, Inf)]
42+
@parameters k [tunable = true, bounds = (0, Inf)]
43+
@parameters k2
44+
eqs = [
45+
Dₜ(x) ~ (-k2*x + k*u) / T
46+
y ~ x
47+
]
48+
sys = ODESystem(eqs, t, name=:tunable_first_order)
49+
50+
p = tunable_parameters(sys)
51+
sp = Set(p)
52+
@test k sp
53+
@test T sp
54+
@test k2 sp
55+
@test length(p) == 2
56+
57+
lb, ub = getbounds(p)
58+
@test lb == [0,0]
59+
@test ub == [Inf, Inf]
60+
61+
b = getbounds(sys)
62+
@test b[T] == (0, Inf)
63+
64+
p = tunable_parameters(sys, default=true)
65+
sp = Set(p)
66+
@test k sp
67+
@test T sp
68+
@test k2 sp
69+
@test length(p) == 3

0 commit comments

Comments
 (0)