Skip to content

Commit 171c43e

Browse files
Merge pull request #2728 from AayushSabharwal/as/discrete-save
feat: implement new discrete saving functionality
2 parents 110ebfd + 3a073ec commit 171c43e

18 files changed

+865
-533
lines changed

Project.toml

Lines changed: 5 additions & 3 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"
@@ -98,18 +100,18 @@ NonlinearSolve = "3.12"
98100
OrderedCollections = "1"
99101
OrdinaryDiffEq = "6.82.0"
100102
PrecompileTools = "1"
101-
RecursiveArrayTools = "2.3, 3"
103+
RecursiveArrayTools = "3.26"
102104
Reexport = "0.2, 1"
103105
RuntimeGeneratedFunctions = "0.5.9"
104-
SciMLBase = "2.28.0"
106+
SciMLBase = "2.46"
105107
SciMLStructures = "1.0"
106108
Serialization = "1"
107109
Setfield = "0.7, 0.8, 1"
108110
SimpleNonlinearSolve = "0.1.0, 1"
109111
SparseArrays = "1"
110112
SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2"
111113
StaticArrays = "0.10, 0.11, 0.12, 1.0"
112-
SymbolicIndexingInterface = "0.3.12"
114+
SymbolicIndexingInterface = "0.3.26"
113115
SymbolicUtils = "2.1"
114116
Symbolics = "5.32"
115117
URIs = "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 & 68 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,4 @@ 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
151-
t::Union{Nothing, Symbolic}
152-
IntegerSequence(t::Union{Num, Symbolic}) = new(value(t))
153-
end
118+
struct IntegerSequence end

src/discretedomain.jl

Lines changed: 29 additions & 24 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
@@ -176,15 +183,18 @@ julia> x(k) # no shift
176183
x(t)
177184
178185
julia> x(k+1) # shift
179-
Shift(t, 1)(x(t))
186+
Shift(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, IntegerSequence} = Inferred, steps::Int = 0)
194+
new(clock, steps)
195+
end
196+
ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps)
197+
ShiftIndex(::Num, steps::Int) = new(IntegerSequence(), steps)
188198
end
189199

190200
function (xn::Num)(k::ShiftIndex)
@@ -197,18 +207,13 @@ function (xn::Num)(k::ShiftIndex)
197207
args = Symbolics.arguments(vars[]) # args should be one element vector with the t in x(t)
198208
length(args) == 1 ||
199209
error("Cannot shift an expression with multiple independent variables $x.")
200-
t = args[]
201-
if hasfield(typeof(clock), :t)
202-
isequal(t, clock.t) ||
203-
error("Independent variable of $xn is not the same as that of the ShiftIndex $(k.t)")
204-
end
205210

206211
# d, _ = propagate_time_domain(xn)
207212
# if d != clock # this is only required if the variable has another clock
208213
# xn = Sample(t, clock)(xn)
209214
# end
210215
# QUESTION: should we return a variable with time domain set to k.clock?
211-
xn = setmetadata(xn, TimeDomain, k.clock)
216+
xn = setmetadata(xn, VariableTimeDomain, k.clock)
212217
if steps == 0
213218
return xn # x(k) needs no shift operator if the step of k is 0
214219
end
@@ -221,37 +226,37 @@ Base.:-(k::ShiftIndex, i::Int) = k + (-i)
221226
"""
222227
input_timedomain(op::Operator)
223228
224-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` operates on.
229+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` operates on.
225230
"""
226231
function input_timedomain(s::Shift, arg = nothing)
227232
if has_time_domain(arg)
228233
return get_time_domain(arg)
229234
end
230-
InferredDiscrete()
235+
InferredDiscrete
231236
end
232237

233238
"""
234239
output_timedomain(op::Operator)
235240
236-
Return the time-domain type (`Continuous()` or `Discrete()`) that `op` results in.
241+
Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` results in.
237242
"""
238243
function output_timedomain(s::Shift, arg = nothing)
239244
if has_time_domain(arg)
240245
return get_time_domain(arg)
241246
end
242-
InferredDiscrete()
247+
InferredDiscrete
243248
end
244249

245-
input_timedomain(::Sample, arg = nothing) = Continuous()
250+
input_timedomain(::Sample, arg = nothing) = Continuous
246251
output_timedomain(s::Sample, arg = nothing) = s.clock
247252

248253
function input_timedomain(h::Hold, arg = nothing)
249254
if has_time_domain(arg)
250255
return get_time_domain(arg)
251256
end
252-
InferredDiscrete() # the Hold accepts any discrete
257+
InferredDiscrete # the Hold accepts any discrete
253258
end
254-
output_timedomain(::Hold, arg = nothing) = Continuous()
259+
output_timedomain(::Hold, arg = nothing) = Continuous
255260

256261
sampletime(op::Sample, arg = nothing) = sampletime(op.clock)
257262
sampletime(op::ShiftIndex, arg = nothing) = sampletime(op.clock)

0 commit comments

Comments
 (0)