Skip to content

Commit e3c62d1

Browse files
refactor: use clock from SciMLBase, fix tests
1 parent 56b1420 commit e3c62d1

13 files changed

+285
-194
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1919
DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf"
2020
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
2121
ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
22+
Expronicon = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636"
2223
FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224"
2324
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
2425
FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf"
@@ -79,6 +80,7 @@ DocStringExtensions = "0.7, 0.8, 0.9"
7980
DomainSets = "0.6, 0.7"
8081
DynamicQuantities = "^0.11.2, 0.12, 0.13"
8182
ExprTools = "0.1.10"
83+
Expronicon = "0.8"
8284
FindFirstFunctions = "1"
8385
ForwardDiff = "0.10.3"
8486
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 = [
@@ -98,7 +98,7 @@ may thus be modeled as
9898

9999
```julia
100100
@variables t y(t) [description = "Output"] u(t) [description = "Input"]
101-
k = ShiftIndex(Clock(t, dt))
101+
k = ShiftIndex(Clock(dt))
102102
eqs = [
103103
a2 * y(k) + a1 * y(k - 1) + a0 * y(k - 2) ~ b2 * u(k) + b1 * u(k - 1) + b0 * u(k - 2)
104104
]
@@ -127,10 +127,10 @@ requires specification of the initial condition for both `x(k-1)` and `x(k-2)`.
127127
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:
128128

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

136136
`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.
@@ -147,7 +147,7 @@ using ModelingToolkit: t_nounits as t
147147
using ModelingToolkit: D_nounits as D
148148
dt = 0.5 # Sample interval
149149
@variables r(t)
150-
clock = Clock(t, dt)
150+
clock = Clock(dt)
151151
k = ShiftIndex(clock)
152152
153153
function plant(; name)

src/ModelingToolkit.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ using SciMLStructures
4242
using Compat
4343
using AbstractTrees
4444
using DiffEqBase, SciMLBase, ForwardDiff
45-
using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap
45+
using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain,
46+
PeriodicClock, Clock, SolverStepClock, Continuous
4647
using Distributed
4748
import JuliaFormatter
4849
using MLStyle
@@ -270,6 +271,6 @@ export debug_system
270271
#export has_discrete_domain, has_continuous_domain
271272
#export is_discrete_domain, is_continuous_domain, is_hybrid_domain
272273
export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime
273-
export Clock #, InferredDiscrete,
274+
export Clock, SolverStepClock, TimeDomain
274275

275276
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> @variables t;
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
@@ -178,11 +185,14 @@ Shift(t, 1)(x(t))
178185
```
179186
"""
180187
struct ShiftIndex
181-
clock::TimeDomain
188+
clock::Union{InferredTimeDomain, TimeDomain, IntegerSequence}
182189
steps::Int
183-
ShiftIndex(clock::TimeDomain = Inferred(), steps::Int = 0) = new(clock, steps)
184-
ShiftIndex(t::Num, dt::Real, steps::Int = 0) = new(Clock(t, dt), steps)
185-
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(t), steps)
190+
function ShiftIndex(
191+
clock::Union{TimeDomain, InferredTimeDomain} = Inferred, steps::Int = 0)
192+
new(clock, steps)
193+
end
194+
ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps)
195+
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(), steps)
186196
end
187197

188198
function (xn::Num)(k::ShiftIndex)
@@ -206,7 +216,7 @@ function (xn::Num)(k::ShiftIndex)
206216
# xn = Sample(t, clock)(xn)
207217
# end
208218
# QUESTION: should we return a variable with time domain set to k.clock?
209-
xn = setmetadata(xn, TimeDomain, k.clock)
219+
xn = setmetadata(xn, VariableTimeDomain, k.clock)
210220
if steps == 0
211221
return xn # x(k) needs no shift operator if the step of k is 0
212222
end
@@ -219,37 +229,37 @@ Base.:-(k::ShiftIndex, i::Int) = k + (-i)
219229
"""
220230
input_timedomain(op::Operator)
221231
222-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` operates on.
232+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` operates on.
223233
"""
224234
function input_timedomain(s::Shift, arg = nothing)
225235
if has_time_domain(arg)
226236
return get_time_domain(arg)
227237
end
228-
InferredDiscrete()
238+
InferredDiscrete
229239
end
230240

231241
"""
232242
output_timedomain(op::Operator)
233243
234-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` results in.
244+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` results in.
235245
"""
236246
function output_timedomain(s::Shift, arg = nothing)
237247
if has_time_domain(arg)
238248
return get_time_domain(arg)
239249
end
240-
InferredDiscrete()
250+
InferredDiscrete
241251
end
242252

243-
input_timedomain(::Sample, arg = nothing) = Continuous()
253+
input_timedomain(::Sample, arg = nothing) = Continuous
244254
output_timedomain(s::Sample, arg = nothing) = s.clock
245255

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

254264
sampletime(op::Sample, arg = nothing) = sampletime(op.clock)
255265
sampletime(op::ShiftIndex, arg = nothing) = sampletime(op.clock)

0 commit comments

Comments
 (0)