Skip to content

Commit c9062fb

Browse files
refactor: use clock from SciMLBase, fix tests
1 parent e560401 commit c9062fb

13 files changed

+285
-194
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
2020
DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf"
2121
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
2222
ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
23+
Expronicon = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636"
2324
FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224"
2425
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
2526
FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf"
@@ -81,6 +82,7 @@ DocStringExtensions = "0.7, 0.8, 0.9"
8182
DomainSets = "0.6, 0.7"
8283
DynamicQuantities = "^0.11.2, 0.12, 0.13"
8384
ExprTools = "0.1.10"
85+
Expronicon = "0.8"
8486
FindFirstFunctions = "1"
8587
ForwardDiff = "0.10.3"
8688
FunctionWrappersWrappers = "0.1"

docs/src/tutorials/SampledData.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A clock can be seen as an *event source*, i.e., when the clock ticks, an event i
1616
- [`Hold`](@ref)
1717
- [`ShiftIndex`](@ref)
1818

19-
When a continuous-time variable `x` is sampled using `xd = Sample(x, dt)`, the result is a discrete-time variable `xd` that is defined and updated whenever the clock ticks. `xd` is *only defined when the clock ticks*, which it does with an interval of `dt`. If `dt` is unspecified, the tick rate of the clock associated with `xd` is inferred from the context in which `xd` appears. Any variable taking part in the same equation as `xd` is inferred to belong to the same *discrete partition* as `xd`, i.e., belonging to the same clock. A system may contain multiple different discrete-time partitions, each with a unique clock. This allows for modeling of multi-rate systems and discrete-time processes located on different computers etc.
19+
When a continuous-time variable `x` is sampled using `xd = Sample(dt)(x)`, the result is a discrete-time variable `xd` that is defined and updated whenever the clock ticks. `xd` is *only defined when the clock ticks*, which it does with an interval of `dt`. If `dt` is unspecified, the tick rate of the clock associated with `xd` is inferred from the context in which `xd` appears. Any variable taking part in the same equation as `xd` is inferred to belong to the same *discrete partition* as `xd`, i.e., belonging to the same clock. A system may contain multiple different discrete-time partitions, each with a unique clock. This allows for modeling of multi-rate systems and discrete-time processes located on different computers etc.
2020

2121
To make a discrete-time variable available to the continuous partition, the [`Hold`](@ref) operator is used. `xc = Hold(xd)` creates a continuous-time variable `xc` that is updated whenever the clock associated with `xd` ticks, and holds its value constant between ticks.
2222

@@ -34,7 +34,7 @@ using ModelingToolkit
3434
using ModelingToolkit: t_nounits as t
3535
@variables x(t) y(t) u(t)
3636
dt = 0.1 # Sample interval
37-
clock = Clock(t, dt) # A periodic clock with tick rate dt
37+
clock = Clock(dt) # A periodic clock with tick rate dt
3838
k = ShiftIndex(clock)
3939
4040
eqs = [
@@ -99,7 +99,7 @@ may thus be modeled as
9999
```julia
100100
t = ModelingToolkit.t_nounits
101101
@variables y(t) [description = "Output"] u(t) [description = "Input"]
102-
k = ShiftIndex(Clock(t, dt))
102+
k = ShiftIndex(Clock(dt))
103103
eqs = [
104104
a2 * y(k) + a1 * y(k - 1) + a0 * y(k - 2) ~ b2 * u(k) + b1 * u(k - 1) + b0 * u(k - 2)
105105
]
@@ -128,10 +128,10 @@ requires specification of the initial condition for both `x(k-1)` and `x(k-2)`.
128128
Multi-rate systems are easy to model using multiple different clocks. The following set of equations is valid, and defines *two different discrete-time partitions*, each with its own clock:
129129

130130
```julia
131-
yd1 ~ Sample(t, dt1)(y)
132-
ud1 ~ kp * (Sample(t, dt1)(r) - yd1)
133-
yd2 ~ Sample(t, dt2)(y)
134-
ud2 ~ kp * (Sample(t, dt2)(r) - yd2)
131+
yd1 ~ Sample(dt1)(y)
132+
ud1 ~ kp * (Sample(dt1)(r) - yd1)
133+
yd2 ~ Sample(dt2)(y)
134+
ud2 ~ kp * (Sample(dt2)(r) - yd2)
135135
```
136136

137137
`yd1` and `ud1` belong to the same clock which ticks with an interval of `dt1`, while `yd2` and `ud2` belong to a different clock which ticks with an interval of `dt2`. The two clocks are *not synchronized*, i.e., they are not *guaranteed* to tick at the same point in time, even if one tick interval is a rational multiple of the other. Mechanisms for synchronization of clocks are not yet implemented.
@@ -148,7 +148,7 @@ using ModelingToolkit: t_nounits as t
148148
using ModelingToolkit: D_nounits as D
149149
dt = 0.5 # Sample interval
150150
@variables r(t)
151-
clock = Clock(t, dt)
151+
clock = Clock(dt)
152152
k = ShiftIndex(clock)
153153
154154
function plant(; name)

src/ModelingToolkit.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ using SciMLStructures
4343
using Compat
4444
using AbstractTrees
4545
using DiffEqBase, SciMLBase, ForwardDiff
46-
using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap
46+
using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain,
47+
PeriodicClock, Clock, SolverStepClock, Continuous
4748
using Distributed
4849
import JuliaFormatter
4950
using MLStyle
@@ -272,6 +273,6 @@ export debug_system
272273
#export has_discrete_domain, has_continuous_domain
273274
#export is_discrete_domain, is_continuous_domain, is_hybrid_domain
274275
export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime
275-
export Clock #, InferredDiscrete,
276+
export Clock, SolverStepClock, TimeDomain
276277

277278
end # module

src/clock.jl

Lines changed: 33 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
1-
abstract type TimeDomain end
2-
abstract type AbstractDiscrete <: TimeDomain end
1+
module InferredClock
32

4-
Base.Broadcast.broadcastable(d::TimeDomain) = Ref(d)
3+
export InferredTimeDomain
54

6-
struct Inferred <: TimeDomain end
7-
struct InferredDiscrete <: AbstractDiscrete end
8-
struct Continuous <: TimeDomain end
5+
using Expronicon.ADT: @adt, @match
6+
using SciMLBase: TimeDomain
97

10-
Symbolics.option_to_metadata_type(::Val{:timedomain}) = TimeDomain
8+
@adt InferredTimeDomain begin
9+
Inferred
10+
InferredDiscrete
11+
end
12+
13+
Base.Broadcast.broadcastable(x::InferredTimeDomain) = Ref(x)
14+
15+
end
16+
17+
using .InferredClock
18+
19+
struct VariableTimeDomain end
20+
Symbolics.option_to_metadata_type(::Val{:timedomain}) = VariableTimeDomain
21+
22+
is_concrete_time_domain(::TimeDomain) = true
23+
is_concrete_time_domain(_) = false
1124

1225
"""
1326
is_continuous_domain(x)
@@ -16,15 +29,15 @@ true if `x` contains only continuous-domain signals.
1629
See also [`has_continuous_domain`](@ref)
1730
"""
1831
function is_continuous_domain(x)
19-
issym(x) && return getmetadata(x, TimeDomain, false) isa Continuous
32+
issym(x) && return getmetadata(x, VariableTimeDomain, false) == Continuous
2033
!has_discrete_domain(x) && has_continuous_domain(x)
2134
end
2235

2336
function get_time_domain(x)
2437
if iscall(x) && operation(x) isa Operator
2538
output_timedomain(x)
2639
else
27-
getmetadata(x, TimeDomain, nothing)
40+
getmetadata(x, VariableTimeDomain, nothing)
2841
end
2942
end
3043
get_time_domain(x::Num) = get_time_domain(value(x))
@@ -37,14 +50,14 @@ Determine if variable `x` has a time-domain attributed to it.
3750
function has_time_domain(x::Symbolic)
3851
# getmetadata(x, Continuous, nothing) !== nothing ||
3952
# getmetadata(x, Discrete, nothing) !== nothing
40-
getmetadata(x, TimeDomain, nothing) !== nothing
53+
getmetadata(x, VariableTimeDomain, nothing) !== nothing
4154
end
4255
has_time_domain(x::Num) = has_time_domain(value(x))
4356
has_time_domain(x) = false
4457

4558
for op in [Differential]
46-
@eval input_timedomain(::$op, arg = nothing) = Continuous()
47-
@eval output_timedomain(::$op, arg = nothing) = Continuous()
59+
@eval input_timedomain(::$op, arg = nothing) = Continuous
60+
@eval output_timedomain(::$op, arg = nothing) = Continuous
4861
end
4962

5063
"""
@@ -83,12 +96,17 @@ true if `x` contains only discrete-domain signals.
8396
See also [`has_discrete_domain`](@ref)
8497
"""
8598
function is_discrete_domain(x)
86-
if hasmetadata(x, TimeDomain) || issym(x)
87-
return getmetadata(x, TimeDomain, false) isa AbstractDiscrete
99+
if hasmetadata(x, VariableTimeDomain) || issym(x)
100+
return is_discrete_time_domain(getmetadata(x, VariableTimeDomain, false))
88101
end
89102
!has_discrete_domain(x) && has_continuous_domain(x)
90103
end
91104

105+
sampletime(c) = @match c begin
106+
PeriodicClock(dt, _...) => dt
107+
_ => nothing
108+
end
109+
92110
struct ClockInferenceException <: Exception
93111
msg::Any
94112
end
@@ -97,57 +115,7 @@ function Base.showerror(io::IO, cie::ClockInferenceException)
97115
print(io, "ClockInferenceException: ", cie.msg)
98116
end
99117

100-
abstract type AbstractClock <: AbstractDiscrete end
101-
102-
"""
103-
Clock <: AbstractClock
104-
Clock([t]; dt)
105-
106-
The default periodic clock with independent variables `t` and tick interval `dt`.
107-
If `dt` is left unspecified, it will be inferred (if possible).
108-
"""
109-
struct Clock <: AbstractClock
110-
"Independent variable"
111-
t::Union{Nothing, Symbolic}
112-
"Period"
113-
dt::Union{Nothing, Float64}
114-
Clock(t::Union{Num, Symbolic}, dt = nothing) = new(value(t), dt)
115-
Clock(t::Nothing, dt = nothing) = new(t, dt)
116-
end
117-
Clock(dt::Real) = Clock(nothing, dt)
118-
Clock() = Clock(nothing, nothing)
119-
120-
sampletime(c) = isdefined(c, :dt) ? c.dt : nothing
121-
Base.hash(c::Clock, seed::UInt) = hash(c.dt, seed 0x953d7a9a18874b90)
122-
function Base.:(==)(c1::Clock, c2::Clock)
123-
((c1.t === nothing || c2.t === nothing) || isequal(c1.t, c2.t)) && c1.dt == c2.dt
124-
end
125-
126-
is_concrete_time_domain(x) = x isa Union{AbstractClock, Continuous}
127-
128-
"""
129-
SolverStepClock <: AbstractClock
130-
SolverStepClock()
131-
SolverStepClock(t)
132-
133-
A clock that ticks at each solver step (sometimes referred to as "continuous sample time"). This clock **does generally not have equidistant tick intervals**, instead, the tick interval depends on the adaptive step-size selection of the continuous solver, as well as any continuous event handling. If adaptivity of the solver is turned off and there are no continuous events, the tick interval will be given by the fixed solver time step `dt`.
134-
135-
Due to possibly non-equidistant tick intervals, this clock should typically not be used with discrete-time systems that assume a fixed sample time, such as PID controllers and digital filters.
136-
"""
137-
struct SolverStepClock <: AbstractClock
138-
"Independent variable"
139-
t::Union{Nothing, Symbolic}
140-
"Period"
141-
SolverStepClock(t::Union{Num, Symbolic}) = new(value(t))
142-
end
143-
SolverStepClock() = SolverStepClock(nothing)
144-
145-
Base.hash(c::SolverStepClock, seed::UInt) = seed 0x953d7b9a18874b91
146-
function Base.:(==)(c1::SolverStepClock, c2::SolverStepClock)
147-
((c1.t === nothing || c2.t === nothing) || isequal(c1.t, c2.t))
148-
end
149-
150-
struct IntegerSequence <: AbstractClock
118+
struct IntegerSequence
151119
t::Union{Nothing, Symbolic}
152120
IntegerSequence(t::Union{Num, Symbolic}) = new(value(t))
153121
end

src/discretedomain.jl

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ $(TYPEDEF)
8585
Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal.
8686
8787
# Constructors
88-
`Sample(clock::TimeDomain = InferredDiscrete())`
89-
`Sample([t], dt::Real)`
88+
`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete)`
89+
`Sample(dt::Real)`
9090
9191
`Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`.
9292
@@ -100,16 +100,23 @@ julia> using Symbolics
100100
101101
julia> t = ModelingToolkit.t_nounits
102102
103-
julia> Δ = Sample(t, 0.01)
103+
julia> Δ = Sample(0.01)
104104
(::Sample) (generic function with 2 methods)
105105
```
106106
"""
107107
struct Sample <: Operator
108108
clock::Any
109-
Sample(clock::TimeDomain = InferredDiscrete()) = new(clock)
110-
Sample(t, dt::Real) = new(Clock(t, dt))
109+
Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete) = new(clock)
110+
end
111+
112+
function Sample(arg::Real)
113+
arg = unwrap(arg)
114+
if symbolic_type(arg) == NotSymbolic()
115+
Sample(Clock(arg))
116+
else
117+
Sample()(arg)
118+
end
111119
end
112-
Sample(x) = Sample()(x)
113120
(D::Sample)(x) = Term{symtype(x)}(D, Any[x])
114121
(D::Sample)(x::Num) = Num(D(value(x)))
115122
SymbolicUtils.promote_symtype(::Sample, x) = x
@@ -180,11 +187,14 @@ Shift(t, 1)(x(t))
180187
```
181188
"""
182189
struct ShiftIndex
183-
clock::TimeDomain
190+
clock::Union{InferredTimeDomain, TimeDomain, IntegerSequence}
184191
steps::Int
185-
ShiftIndex(clock::TimeDomain = Inferred(), steps::Int = 0) = new(clock, steps)
186-
ShiftIndex(t::Num, dt::Real, steps::Int = 0) = new(Clock(t, dt), steps)
187-
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(t), steps)
192+
function ShiftIndex(
193+
clock::Union{TimeDomain, InferredTimeDomain} = Inferred, steps::Int = 0)
194+
new(clock, steps)
195+
end
196+
ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps)
197+
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(), steps)
188198
end
189199

190200
function (xn::Num)(k::ShiftIndex)
@@ -208,7 +218,7 @@ function (xn::Num)(k::ShiftIndex)
208218
# xn = Sample(t, clock)(xn)
209219
# end
210220
# QUESTION: should we return a variable with time domain set to k.clock?
211-
xn = setmetadata(xn, TimeDomain, k.clock)
221+
xn = setmetadata(xn, VariableTimeDomain, k.clock)
212222
if steps == 0
213223
return xn # x(k) needs no shift operator if the step of k is 0
214224
end
@@ -221,37 +231,37 @@ Base.:-(k::ShiftIndex, i::Int) = k + (-i)
221231
"""
222232
input_timedomain(op::Operator)
223233
224-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` operates on.
234+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` operates on.
225235
"""
226236
function input_timedomain(s::Shift, arg = nothing)
227237
if has_time_domain(arg)
228238
return get_time_domain(arg)
229239
end
230-
InferredDiscrete()
240+
InferredDiscrete
231241
end
232242

233243
"""
234244
output_timedomain(op::Operator)
235245
236-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` results in.
246+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` results in.
237247
"""
238248
function output_timedomain(s::Shift, arg = nothing)
239249
if has_time_domain(arg)
240250
return get_time_domain(arg)
241251
end
242-
InferredDiscrete()
252+
InferredDiscrete
243253
end
244254

245-
input_timedomain(::Sample, arg = nothing) = Continuous()
255+
input_timedomain(::Sample, arg = nothing) = Continuous
246256
output_timedomain(s::Sample, arg = nothing) = s.clock
247257

248258
function input_timedomain(h::Hold, arg = nothing)
249259
if has_time_domain(arg)
250260
return get_time_domain(arg)
251261
end
252-
InferredDiscrete() # the Hold accepts any discrete
262+
InferredDiscrete # the Hold accepts any discrete
253263
end
254-
output_timedomain(::Hold, arg = nothing) = Continuous()
264+
output_timedomain(::Hold, arg = nothing) = Continuous
255265

256266
sampletime(op::Sample, arg = nothing) = sampletime(op.clock)
257267
sampletime(op::ShiftIndex, arg = nothing) = sampletime(op.clock)

0 commit comments

Comments
 (0)