Skip to content

Commit 9044096

Browse files
refactor: use clock from SciMLBase, fix tests
1 parent 6133258 commit 9044096

13 files changed

+283
-192
lines changed

Project.toml

+2
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

+8-8
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

+3-2
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

+33-65
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

+28-18
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ $(TYPEDEF)
8080
Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal.
8181
8282
# Constructors
83-
`Sample(clock::TimeDomain = InferredDiscrete())`
84-
`Sample([t], dt::Real)`
83+
`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete)`
84+
`Sample(dt::Real)`
8585
8686
`Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`.
8787
@@ -95,16 +95,23 @@ julia> using Symbolics
9595
9696
julia> @variables t;
9797
98-
julia> Δ = Sample(t, 0.01)
98+
julia> Δ = Sample(0.01)
9999
(::Sample) (generic function with 2 methods)
100100
```
101101
"""
102102
struct Sample <: Operator
103103
clock::Any
104-
Sample(clock::TimeDomain = InferredDiscrete()) = new(clock)
105-
Sample(t, dt::Real) = new(Clock(t, dt))
104+
Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete) = new(clock)
105+
end
106+
107+
function Sample(arg::Real)
108+
arg = unwrap(arg)
109+
if symbolic_type(arg) == NotSymbolic()
110+
Sample(Clock(arg))
111+
else
112+
Sample()(arg)
113+
end
106114
end
107-
Sample(x) = Sample()(x)
108115
(D::Sample)(x) = Term{symtype(x)}(D, Any[x])
109116
(D::Sample)(x::Num) = Num(D(value(x)))
110117
SymbolicUtils.promote_symtype(::Sample, x) = x
@@ -169,11 +176,14 @@ Shift(t, 1)(x(t))
169176
```
170177
"""
171178
struct ShiftIndex
172-
clock::TimeDomain
179+
clock::Union{InferredTimeDomain, TimeDomain, IntegerSequence}
173180
steps::Int
174-
ShiftIndex(clock::TimeDomain = Inferred(), steps::Int = 0) = new(clock, steps)
175-
ShiftIndex(t::Num, dt::Real, steps::Int = 0) = new(Clock(t, dt), steps)
176-
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(t), steps)
181+
function ShiftIndex(
182+
clock::Union{TimeDomain, InferredTimeDomain} = Inferred, steps::Int = 0)
183+
new(clock, steps)
184+
end
185+
ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps)
186+
ShiftIndex(t::Num, steps::Int = 0) = new(IntegerSequence(), steps)
177187
end
178188

179189
function (xn::Num)(k::ShiftIndex)
@@ -197,7 +207,7 @@ function (xn::Num)(k::ShiftIndex)
197207
# xn = Sample(t, clock)(xn)
198208
# end
199209
# QUESTION: should we return a variable with time domain set to k.clock?
200-
xn = setmetadata(xn, TimeDomain, k.clock)
210+
xn = setmetadata(xn, VariableTimeDomain, k.clock)
201211
if steps == 0
202212
return xn # x(k) needs no shift operator if the step of k is 0
203213
end
@@ -210,37 +220,37 @@ Base.:-(k::ShiftIndex, i::Int) = k + (-i)
210220
"""
211221
input_timedomain(op::Operator)
212222
213-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` operates on.
223+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` operates on.
214224
"""
215225
function input_timedomain(s::Shift, arg = nothing)
216226
if has_time_domain(arg)
217227
return get_time_domain(arg)
218228
end
219-
InferredDiscrete()
229+
InferredDiscrete
220230
end
221231

222232
"""
223233
output_timedomain(op::Operator)
224234
225-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` results in.
235+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` results in.
226236
"""
227237
function output_timedomain(s::Shift, arg = nothing)
228238
if has_time_domain(arg)
229239
return get_time_domain(arg)
230240
end
231-
InferredDiscrete()
241+
InferredDiscrete
232242
end
233243

234-
input_timedomain(::Sample, arg = nothing) = Continuous()
244+
input_timedomain(::Sample, arg = nothing) = Continuous
235245
output_timedomain(s::Sample, arg = nothing) = s.clock
236246

237247
function input_timedomain(h::Hold, arg = nothing)
238248
if has_time_domain(arg)
239249
return get_time_domain(arg)
240250
end
241-
InferredDiscrete() # the Hold accepts any discrete
251+
InferredDiscrete # the Hold accepts any discrete
242252
end
243-
output_timedomain(::Hold, arg = nothing) = Continuous()
253+
output_timedomain(::Hold, arg = nothing) = Continuous
244254

245255
sampletime(op::Sample, arg = nothing) = sampletime(op.clock)
246256
sampletime(op::ShiftIndex, arg = nothing) = sampletime(op.clock)

0 commit comments

Comments
 (0)