Skip to content

Commit 0c833cb

Browse files
Merge pull request #153 from saschatimme/st/variable-arrays
Add easy possibility to create array of variables
2 parents 93fe6bd + 769e724 commit 0c833cb

File tree

3 files changed

+114
-18
lines changed

3 files changed

+114
-18
lines changed

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ f = @eval eval(nlsys_func)
109109
f2 = (du,u) -> f(du,u,(10.0,26.0,2.33))
110110
```
111111
112+
### Example: Arrays of variables
113+
114+
Sometimes it is convenient to define arrays of variables to model things like `x₁,…,x₃`.
115+
The `@variables` and `@parameters` macros support this with the following syntax:
116+
117+
```julia
118+
julia> @variables x[1:3];
119+
julia> x
120+
3-element Array{Operation,1}:
121+
x[1]()
122+
x[2]()
123+
x[3]()
124+
125+
# support for arbitrary ranges and tensors
126+
julia> @variables y[2:3,1:5:6];
127+
julia> y
128+
2×2 Array{Operation,2}:
129+
y[2,1]() y[2,6]()
130+
y[3,1]() y[3,6]()
131+
132+
133+
# also works for dependent variables
134+
julia> @parameters t; @variables z[1:3](t);
135+
julia> z
136+
3-element Array{Operation,1}:
137+
z[1](t())
138+
z[2](t())
139+
z[3](t())
140+
```
141+
112142
## Core Principles
113143
114144
The core idea behind ModelingToolkit.jl is that mathematical equations require
@@ -145,7 +175,9 @@ context-aware single variable of the IR. Its fields are described as follows:
145175
146176
For example, the following code defines an independent variable `t`, a parameter
147177
`α`, a function parameter `σ`, a variable `x` which depends on `t`, a variable
148-
`y` with no dependents, and a variable `z` which depends on `t`, `α`, and `x(t)`.
178+
`y` with no dependents, a variable `z` which depends on `t`, `α`, and `x(t)`
179+
and a parameters `β₁` and `β₂`.
180+
149181
150182
```julia
151183
t = Variable(:t; known = true)() # independent variables are treated as known
@@ -155,19 +187,21 @@ w = Variable(:w; known = false) # unknown, left uncalled
155187
x = Variable(:x; known = false)(t) # unknown, depends on `t`
156188
y = Variable(:y; known = false)() # unknown, no dependents
157189
z = Variable(:z; known = false)(t, α, x) # unknown, multiple arguments
190+
β₁ = Variable(, 1; known = true)() # with index 1
191+
β₂ = Variable(, 2; known = true)() # with index 2
158192

159-
expr = x + y^α + σ(3) * (z - t) - w(t - 1)
193+
expr = β₁ * x + y^α + σ(3) * (z - t) - β₂ * w(t - 1)
160194
```
161195
162196
We can rewrite this more concisely using macros. Note the difference between
163197
including and excluding empty parentheses. When in call format, variables are
164198
aliased to the given call, allowing implicit use of dependents for convenience.
165199
166200
```julia
167-
@parameters t α σ(..)
201+
@parameters t α σ(..) β[1:2]
168202
@variables w(..) x(t) y() z(t, α, x)
169203

170-
expr = x + y^α + σ(3) * (z - t) - w(t - 1)
204+
expr = β[1] * x + y^α + σ(3) * (z - t) - β[2] * w(t - 1)
171205
```
172206
173207
Note that `@parameters` and `@variables` implicitly add `()` to values that
@@ -276,7 +310,7 @@ is accessible via a function-based interface. This means that all macros are
276310
syntactic sugar in some form. For example, the variable construction:
277311
278312
```julia
279-
@parameters t σ ρ β
313+
@parameters t σ ρ β[1:3]
280314
@variables x(t) y(t) z(t)
281315
@derivatives D'~t
282316
```
@@ -287,7 +321,7 @@ is syntactic sugar for:
287321
t = Variable(:t; known = true)()
288322
σ = Variable(; known = true)
289323
ρ = Variable(; known = true)()
290-
β = Variable(; known = true)()
324+
β = [Variable(, i; known = true)() for i in 1:3]
291325
x = Variable(:x)(t)
292326
y = Variable(:y)(t)
293327
z = Variable(:z)(t)

src/variables.jl

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ struct Variable <: Function
1919
known::Bool
2020
Variable(name; known = false) = new(name, known)
2121
end
22+
function Variable(name, indices...; known = false)
23+
var_name = Symbol("$name[$(join(indices, ","))]")
24+
Variable(var_name; known=known)
25+
end
26+
2227
(x::Variable)(args...) = Operation(x, collect(Expression, args))
2328

2429
Base.isequal(x::Variable, y::Variable) = (x.name, x.known) == (y.name, y.known)
@@ -74,29 +79,64 @@ function _parse_vars(macroname, known, x)
7479
x = flatten_expr!(x)
7580
for _var in x
7681
iscall = isa(_var, Expr) && _var.head == :call
77-
issym = _var isa Symbol
78-
@assert iscall || issym "@$macroname expects a tuple of expressions or an expression of a tuple (`@$macroname x y z(t)` or `@$macroname x, y, z(t)`)"
82+
isarray = isa(_var, Expr) && _var.head == :ref
83+
issym = _var isa Symbol
84+
@assert iscall || isarray || issym "@$macroname expects a tuple of expressions or an expression of a tuple (`@$macroname x y z(t) v[1:3] w[1:2,1:4]` or `@$macroname x, y, z(t) v[1:3] w[1:2,1:4]`)"
7985

8086
if iscall
81-
var_name = _var.args[1]
82-
if _var.args[end] == :..
83-
expr = :($var_name = $Variable($(Meta.quot(var_name)); known = $known))
84-
else
85-
expr = :($var_name = $Variable($(Meta.quot(var_name)); known = $known)($(_var.args[2:end]...)))
86-
end
87+
var_name, expr = _construct_vars(_var.args[1], known, _var.args[2:end])
8788
else
88-
# Implicit 0-args call
89-
var_name = _var
90-
expr = :($var_name = $Variable($(Meta.quot(var_name)); known = $known)())
89+
var_name, expr = _construct_vars(_var, known, nothing)
9190
end
92-
9391
push!(var_names, var_name)
9492
push!(ex.args, expr)
9593
end
9694
push!(ex.args, build_expr(:tuple, var_names))
9795
return ex
9896
end
9997

98+
function _construct_vars(_var, known, call_args)
99+
issym = _var isa Symbol
100+
isarray = isa(_var, Expr) && _var.head == :ref
101+
if isarray
102+
var_name = _var.args[1]
103+
indices = _var.args[2:end]
104+
expr = _construct_array_vars(var_name, known, call_args, indices...)
105+
else
106+
# Implicit 0-args call
107+
var_name = _var
108+
expr = _construct_var(var_name, known, call_args)
109+
end
110+
var_name, :($var_name = $expr)
111+
end
112+
113+
function _construct_var(var_name, known, call_args)
114+
if call_args === nothing
115+
:(Variable($(Meta.quot(var_name)); known = $known)())
116+
elseif call_args[end] == :..
117+
:(Variable($(Meta.quot(var_name)); known = $known))
118+
else
119+
:(Variable($(Meta.quot(var_name)); known = $known)($(call_args...)))
120+
end
121+
end
122+
123+
function _construct_var(var_name, known, call_args, ind)
124+
if call_args === nothing
125+
:(Variable($(Meta.quot(var_name)), $ind...; known = $known)())
126+
elseif call_args[end] == :..
127+
:(Variable($(Meta.quot(var_name)), $ind...; known = $known))
128+
else
129+
:(Variable($(Meta.quot(var_name)), $ind...; known = $known)($(call_args...)))
130+
end
131+
end
132+
133+
134+
function _construct_array_vars(var_name, known, call_args, indices...)
135+
:(map(Iterators.product($(indices...))) do ind
136+
$(_construct_var(var_name, known, call_args, :ind))
137+
end)
138+
end
139+
100140

101141
"""
102142
$(SIGNATURES)

test/variable_parsing.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,25 @@ D1 = Differential(t)
3434
@test @macroexpand(@variables x, y, z(t)) == @macroexpand(@variables x y z(t))
3535

3636
convert(Expression, :($x == 0 ? $y : $x))
37+
38+
# Test array expressions
39+
@parameters begin
40+
t[1:2]
41+
s[1:2:4,1:2]
42+
end
43+
@parameters σ[1:2](..)
44+
45+
t1 = [Variable(:t, 1; known = true)(),
46+
Variable(:t, 2; known = true)()]
47+
s1 = [Variable(:s, 1, 1; known = true)() Variable(:s, 1, 2; known = true)()
48+
Variable(:s, 3, 1; known = true)() Variable(:s, 3, 2; known = true)()]
49+
σ1 = [Variable(, 1; known = true),
50+
Variable(, 2; known = true)]
51+
@test isequal(t1, t)
52+
@test isequal(s1, s)
53+
@test isequal(σ1, σ)
54+
55+
@parameters t
56+
@variables x[1:2](t)
57+
x1 = [Variable(:x, 1)(t), Variable(:x, 2)(t)]
58+
@test isequal(x1, x)

0 commit comments

Comments
 (0)