Skip to content

Commit cd22d89

Browse files
YingboMabaggepinnen
andcommitted
Add TimeDomain, Clock, and their inference
Co-authored-by: Fredrik Bagge Carlson <[email protected]>
1 parent 98e4621 commit cd22d89

File tree

7 files changed

+475
-1
lines changed

7 files changed

+475
-1
lines changed

src/ModelingToolkit.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,11 @@ include("systems/sparsematrixclil.jl")
145145
include("systems/discrete_system/discrete_system.jl")
146146
include("systems/validation.jl")
147147
include("systems/dependency_graphs.jl")
148+
include("clock.jl")
149+
include("discretedomain.jl")
148150
include("systems/systemstructure.jl")
149151
using .SystemStructures
152+
include("systems/clock_inference.jl")
150153

151154
include("debugging.jl")
152155
include("systems/alias_elimination.jl")
@@ -214,4 +217,10 @@ export @variables, @parameters
214217
export @named, @nonamespace, @namespace, extend, compose, complete
215218
export debug_system
216219

220+
export Continuous, Discrete, sampletime, input_timedomain, output_timedomain
221+
#export has_discrete_domain, has_continuous_domain
222+
#export is_discrete_domain, is_continuous_domain, is_hybrid_domain
223+
export Sample, Hold, Shift, ShiftIndex
224+
export Clock #, InferredDiscrete,
225+
217226
end # module

src/clock.jl

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
abstract type TimeDomain end
2+
abstract type AbstractDiscrete <: TimeDomain end
3+
4+
Base.Broadcast.broadcastable(d::TimeDomain) = Ref(d)
5+
6+
struct Inferred <: TimeDomain end
7+
struct InferredDiscrete <: AbstractDiscrete end
8+
struct Continuous <: TimeDomain end
9+
10+
const UnknownDomain = Union{Nothing, Inferred, InferredDiscrete}
11+
const InferredDomain = Union{Inferred, InferredDiscrete}
12+
13+
Symbolics.option_to_metadata_type(::Val{:timedomain}) = TimeDomain
14+
15+
"""
16+
is_continuous_domain(x::Sym)
17+
Determine if variable `x` is a continuous-time variable.
18+
"""
19+
is_continuous_domain(x::Sym) = getmetadata(x, TimeDomain, false) isa Continuous
20+
21+
"""
22+
is_discrete_domain(x::Sym)
23+
Determine if variable `x` is a discrete-time variable.
24+
"""
25+
is_discrete_domain(x::Sym) = getmetadata(x, TimeDomain, false) isa Discrete
26+
27+
# is_discrete_domain(x::Sym) = isvarkind(Discrete, x)
28+
29+
has_continuous_domain(x::Sym) = is_continuous_domain(x)
30+
has_discrete_domain(x::Sym) = is_discrete_domain(x)
31+
32+
function get_time_domain(x)
33+
if istree(x) && operation(x) isa Operator
34+
output_timedomain(x)
35+
else
36+
getmetadata(x, TimeDomain, nothing)
37+
end
38+
end
39+
get_time_domain(x::Num) = get_time_domain(value(x))
40+
41+
"""
42+
has_time_domain(x::Sym)
43+
Determine if variable `x` has a time-domain attributed to it.
44+
"""
45+
function has_time_domain(x::Union{Sym, Term})
46+
# getmetadata(x, Continuous, nothing) !== nothing ||
47+
# getmetadata(x, Discrete, nothing) !== nothing
48+
getmetadata(x, TimeDomain, nothing) !== nothing
49+
end
50+
has_time_domain(x::Num) = has_time_domain(value(x))
51+
has_time_domain(x) = false
52+
53+
for op in [Differential, Difference]
54+
@eval input_timedomain(::$op, arg = nothing) = Continuous()
55+
@eval output_timedomain(::$op, arg = nothing) = Continuous()
56+
end
57+
58+
"""
59+
has_discrete_domain(x)
60+
true if `x` contains discrete signals (`x` may or may not contain continuous-domain signals). `x` may be an expression or equation.
61+
See also [`is_discrete_domain`](@ref)
62+
"""
63+
has_discrete_domain(x) = hasshift(x) || hassample(x) || hashold(x)
64+
65+
"""
66+
has_continuous_domain(x)
67+
true if `x` contains continuous signals (`x` may or may not contain discrete-domain signals). `x` may be an expression or equation.
68+
See also [`is_continuous_domain`](@ref)
69+
"""
70+
has_continuous_domain(x) = hasderiv(x) || hasdiff(x) || hassample(x) || hashold(x)
71+
72+
"""
73+
is_hybrid_domain(x)
74+
true if `x` contains both discrete and continuous-domain signals. `x` may be an expression or equation.
75+
"""
76+
is_hybrid_domain(x) = has_discrete_domain(x) && has_continuous_domain(x)
77+
78+
"""
79+
is_discrete_domain(x)
80+
true if `x` contains only discrete-domain signals.
81+
See also [`has_discrete_domain`](@ref)
82+
"""
83+
is_discrete_domain(x) = has_discrete_domain(x) && !has_continuous_domain(x)
84+
85+
"""
86+
is_continuous_domain(x)
87+
true if `x` contains only continuous-domain signals.
88+
See also [`has_continuous_domain`](@ref)
89+
"""
90+
is_continuous_domain(x) = !has_discrete_domain(x) && has_continuous_domain(x)
91+
92+
abstract type AbstractClock <: AbstractDiscrete end
93+
94+
"""
95+
Clock <: AbstractClock
96+
Clock(t; dt)
97+
The default periodic clock with independent variables `t` and tick interval `dt`.
98+
If `dt` is left unspecified, it will be inferred (if possible).
99+
"""
100+
struct Clock <: AbstractClock
101+
"Independent variable"
102+
t::Any
103+
"Period"
104+
dt::Any
105+
Clock(t, dt = nothing) = new(value(t), dt)
106+
end
107+
108+
sampletime(c) = isdefined(c, :dt) ? c.dt : nothing

src/discretedomain.jl

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
using Symbolics: Operator, Num, Term, value, recursive_hasoperator
2+
3+
# Shift
4+
5+
"""
6+
$(TYPEDEF)
7+
8+
Represents a shift operator.
9+
10+
# Fields
11+
$(FIELDS)
12+
13+
# Examples
14+
15+
```jldoctest
16+
julia> using Symbolics
17+
18+
julia> @variables t;
19+
20+
julia> Δ = Shift(t)
21+
(::Shift) (generic function with 2 methods)
22+
```
23+
"""
24+
struct Shift <: Operator
25+
"""Fixed Shift"""
26+
t::Any
27+
steps::Int
28+
Shift(t, steps = 1) = new(value(t), steps)
29+
end
30+
function (D::Shift)(x, allow_zero = false)
31+
!allow_zero && D.steps == 0 && return x
32+
Term{symtype(x)}(D, Any[x])
33+
end
34+
function (D::Shift)(x::Num, allow_zero = false)
35+
!allow_zero && D.steps == 0 && return x
36+
vt = value(x)
37+
if istree(vt)
38+
op = operation(vt)
39+
if op isa Shift
40+
if isequal(D.t, op.t)
41+
arg = arguments(vt)[1]
42+
newsteps = D.steps + op.steps
43+
return Num(newsteps == 0 ? arg : Shift(D.t, newsteps)(arg))
44+
end
45+
end
46+
end
47+
Num(D(vt, allow_zero))
48+
end
49+
SymbolicUtils.promote_symtype(::Shift, t) = t
50+
51+
Base.show(io::IO, D::Shift) = print(io, "Shift(", D.t, ", ", D.steps, ")")
52+
53+
Base.:(==)(D1::Shift, D2::Shift) = isequal(D1.t, D2.t) && isequal(D1.steps, D2.steps)
54+
Base.hash(D::Shift, u::UInt) = hash(D.steps, hash(D.t, xor(u, 0x055640d6d952f101)))
55+
56+
Base.:^(D::Shift, n::Integer) = Shift(D.t, D.steps * n)
57+
Base.literal_pow(f::typeof(^), D::Shift, ::Val{n}) where {n} = Shift(D.t, D.steps * n)
58+
59+
hasshift(eq::Equation) = hasshift(eq.lhs) || hasshift(eq.rhs)
60+
61+
"""
62+
hasshift(O)
63+
64+
Returns true if the expression or equation `O` contains [`Shift`](@ref) terms.
65+
"""
66+
hasshift(O) = recursive_hasoperator(Shift, O)
67+
68+
# Sample
69+
70+
"""
71+
$(TYPEDEF)
72+
73+
Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal.
74+
75+
# Constructors
76+
`Sample(clock::TimeDomain = InferredDiscrete())`
77+
`Sample(t, dt::Real)`
78+
79+
`Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`.
80+
81+
# Fields
82+
$(FIELDS)
83+
84+
# Examples
85+
86+
```jldoctest
87+
julia> using Symbolics
88+
89+
julia> @variables t;
90+
91+
julia> Δ = Sample(t, 0.01)
92+
(::Sample) (generic function with 2 methods)
93+
```
94+
"""
95+
struct Sample <: Operator
96+
clock::Any
97+
Sample(clock::TimeDomain = InferredDiscrete()) = new(clock)
98+
Sample(t, dt::Real) = new(Clock(t, dt))
99+
end
100+
Sample(x) = Sample()(x)
101+
(D::Sample)(x) = Term{symtype(x)}(D, Any[x])
102+
(D::Sample)(x::Num) = Num(D(value(x)))
103+
SymbolicUtils.promote_symtype(::Sample, x) = x
104+
105+
Base.show(io::IO, D::Sample) = print(io, "Sample(", D.clock, ")")
106+
107+
Base.:(==)(D1::Sample, D2::Sample) = isequal(D1.clock, D2.clock)
108+
Base.hash(D::Sample, u::UInt) = hash(D.clock, xor(u, 0x055640d6d952f101))
109+
110+
"""
111+
hassample(O)
112+
113+
Returns true if the expression or equation `O` contains [`Sample`](@ref) terms.
114+
"""
115+
hassample(O) = recursive_hasoperator(Sample, O)
116+
117+
# Hold
118+
119+
"""
120+
$(TYPEDEF)
121+
122+
Represents a hold operator. A continuous-time signal is produced by holding a discrete-time signal `x` with zero-order hold.
123+
124+
```
125+
cont_x = Hold()(disc_x)
126+
```
127+
"""
128+
struct Hold <: Operator
129+
end
130+
(D::Hold)(x) = Term{symtype(x)}(D, Any[x])
131+
(D::Hold)(x::Num) = Num(D(value(x)))
132+
SymbolicUtils.promote_symtype(::Hold, x) = x
133+
134+
Hold(x) = Hold()(x)
135+
136+
"""
137+
hashold(O)
138+
139+
Returns true if the expression or equation `O` contains [`Hold`](@ref) terms.
140+
"""
141+
hashold(O) = recursive_hasoperator(Hold, O)
142+
143+
# ShiftIndex
144+
145+
"""
146+
ShiftIndex
147+
148+
The `ShiftIndex` operator allows you to index a signal and obtain a shifted discrete-time signal. If the signal is continuous-time, the signal is sampled before shifting.
149+
150+
# Examples
151+
```
152+
julia> @variables t x(t);
153+
154+
julia> k = ShiftIndex(t, 0.1);
155+
156+
julia> x(k) # no shift
157+
x(t)
158+
159+
julia> x(k+1) # shift
160+
Shift(t, 1)(x(t))
161+
```
162+
"""
163+
struct ShiftIndex
164+
clock::TimeDomain
165+
steps::Int
166+
ShiftIndex(clock::TimeDomain = Inferred(), steps::Int = 0) = new(clock, steps)
167+
ShiftIndex(t::Num, dt::Real, steps::Int = 0) = new(Clock(t, dt), steps)
168+
end
169+
170+
function (xn::Num)(k::ShiftIndex)
171+
@unpack clock, steps = k
172+
x = value(xn)
173+
t = clock.t
174+
# Verify that the independent variables of k and x match and that the expression doesn't have multiple variables
175+
vars = Symbolics.get_variables(x)
176+
length(vars) == 1 ||
177+
error("Cannot shift a multivariate expression $x. Either create a new state and shift this, or shift the individual variables in the expression.")
178+
args = Symbolics.arguments(vars[]) # args should be one element vector with the t in x(t)
179+
length(args) == 1 ||
180+
error("Cannot shift an expression with multiple independent variables $x.")
181+
isequal(args[], t) ||
182+
error("Independent variable of $xn is not the same as that of the ShiftIndex $(k.t)")
183+
184+
# d, _ = propagate_time_domain(xn)
185+
# if d != clock # this is only required if the variable has another clock
186+
# xn = Sample(t, clock)(xn)
187+
# end
188+
# QUESTION: should we return a variable with time domain set to k.clock?
189+
xn = setmetadata(xn, TimeDomain, k.clock)
190+
if steps == 0
191+
return xn # x(k) needs no shift operator if the step of k is 0
192+
end
193+
Shift(t, steps)(xn) # a shift of k steps
194+
end
195+
196+
Base.:+(k::ShiftIndex, i::Int) = ShiftIndex(k.clock, k.steps + i)
197+
Base.:-(k::ShiftIndex, i::Int) = k + (-i)
198+
199+
"""
200+
input_timedomain(op::Operator)
201+
202+
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` operates on.
203+
"""
204+
function input_timedomain(s::Shift, arg = nothing)
205+
if has_time_domain(arg)
206+
return get_time_domain(arg)
207+
end
208+
InferredDiscrete()
209+
end
210+
211+
"""
212+
output_timedomain(op::Operator)
213+
214+
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` results in.
215+
"""
216+
function output_timedomain(s::Shift, arg = nothing)
217+
if has_time_domain(arg)
218+
return get_time_domain(arg)
219+
end
220+
InferredDiscrete()
221+
end
222+
223+
input_timedomain(::Sample, arg = nothing) = Continuous()
224+
output_timedomain(s::Sample, arg = nothing) = s.clock
225+
226+
function input_timedomain(h::Hold, arg = nothing)
227+
if has_time_domain(arg)
228+
return get_time_domain(arg)
229+
end
230+
InferredDiscrete() # the Hold accepts any discrete
231+
end
232+
output_timedomain(::Hold, arg = nothing) = Continuous()
233+
234+
sampletime(op::Sample, arg = nothing) = sampletime(op.clock)
235+
sampletime(op::ShiftIndex, arg = nothing) = sampletime(op.clock)
236+
237+
changes_domain(op) = isoperator(op, Union{Sample, Hold})
238+
239+
function output_timedomain(x)
240+
if isoperator(x, Operator)
241+
return output_timedomain(operation(x), arguments(x)[])
242+
else
243+
throw(ArgumentError("$x of type $(typeof(x)) is not an operator expression"))
244+
end
245+
end
246+
247+
function input_timedomain(x)
248+
if isoperator(x, Operator)
249+
return input_timedomain(operation(x), arguments(x)[])
250+
else
251+
throw(ArgumentError("$x of type $(typeof(x)) is not an operator expression"))
252+
end
253+
end

0 commit comments

Comments
 (0)