You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A sampled-data system contains both continuous-time and discrete-time components, such as a continuous-time plant model and a discrete-time control system. ModelingToolkit supports the modeling and simulation of sampled-data systems by means of *clocks*.
3
2
3
+
A sampled-data system contains both continuous-time and discrete-time components, such as a continuous-time plant model and a discrete-time control system. ModelingToolkit supports the modeling and simulation of sampled-data systems by means of *clocks*.
4
4
5
5
!!! danger "Experimental"
6
-
The sampled-data interface is currently experimental and at any time subject to breaking changes **not** respecting semantic versioning.
6
+
7
+
The sampled-data interface is currently experimental and at any time subject to breaking changes **not** respecting semantic versioning.
8
+
9
+
!!! note "Negative shifts"
10
+
11
+
The initial release of the sampled-data interface **only supports negative shifts**.
12
+
13
+
A clock can be seen as an *event source*, i.e., when the clock ticks, an event is generated. In response to the event the discrete-time logic is executed, for example, a control signal is computed. For basic modeling of sampled-data systems, the user does not have to interact with clocks explicitly, instead, the modeling is performed using the operators
7
14
8
-
A clock can be seen as an *even source*, i.e., when the clock ticks, an even is generated. In response to the event the discrete-time logic is executed, for example, a control signal is computed. For basic modeling of sampled-data systems, the user does not have to interact with clocks explicitly, instead, the modeling is performed using the operators
9
-
-[`Sample`](@ref)
10
-
-[`Hold`](@ref)
11
-
-[`ShiftIndex`](@ref)
15
+
-[`Sample`](@ref)
16
+
-[`Hold`](@ref)
17
+
-[`ShiftIndex`](@ref)
12
18
13
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.
14
20
15
-
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.
21
+
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.
16
22
17
23
The operators [`Sample`](@ref) and [`Hold`](@ref) are thus providing the interface between continuous and discrete partitions.
18
24
19
25
The [`ShiftIndex`](@ref) operator is used to refer to past and future values of discrete-time variables. The example below illustrates its use, implementing the discrete-time system
26
+
20
27
```math
21
28
x(k+1) = 0.5x(k) + u(k)
22
29
y(k) = x(k)
23
30
```
31
+
24
32
```@example clocks
25
33
@variables t x(t) y(t) u(t)
26
34
dt = 0.1 # Sample interval
27
35
clock = Clock(t, dt) # A periodic clock with tick rate dt
28
36
k = ShiftIndex(clock)
29
37
30
38
eqs = [
31
-
x(k+1) ~ 0.5x(k) + u(k),
32
-
y ~ x
39
+
x(k) ~ 0.5x(k - 1) + u(k - 1),
40
+
y(k) ~ x(k - 1)
33
41
]
34
42
```
43
+
35
44
A few things to note in this basic example:
36
-
-`x` and `u` are automatically inferred to be discrete-time variables, since they appear in an equation with a discrete-time [`ShiftIndex`](@ref)`k`.
37
-
-`y` is also automatically inferred to be a discrete-time-time variable, since it appears in an equation with another discrete-time variable `x`. `x,u,y` all belong to the same discrete-time partition, i.e., they are all updated at the same *instantaneous point in time* at which the clock ticks.
38
-
- The equation `y ~ x` does not use any shift index, this is equivalent to `y(k) ~ x(k)`, i.e., discrete-time variables without shift index are assumed to refer to the variable at the current time step.
39
-
- The equation `x(k+1) ~ 0.5x(k) + u(k)` indicates how `x` is updated, i.e., what the value of `x` will be at the *next* time step. The output `y`, however, is given by the value of `x` at the *current* time step, i.e., `y(k) ~ x(k)`. If this logic was implemented in an imperative programming style, the logic would thus be
45
+
46
+
- The equation `x(k+1) = 0.5x(k) + u(k)` has been rewritten in terms of negative shifts since positive shifts are not yet supported.
47
+
-`x` and `u` are automatically inferred to be discrete-time variables, since they appear in an equation with a discrete-time [`ShiftIndex`](@ref)`k`.
48
+
-`y` is also automatically inferred to be a discrete-time-time variable, since it appears in an equation with another discrete-time variable `x`. `x,u,y` all belong to the same discrete-time partition, i.e., they are all updated at the same *instantaneous point in time* at which the clock ticks.
49
+
- The equation `y ~ x` does not use any shift index, this is equivalent to `y(k) ~ x(k)`, i.e., discrete-time variables without shift index are assumed to refer to the variable at the current time step.
50
+
- The equation `x(k) ~ 0.5x(k-1) + u(k-1)` indicates how `x` is updated, i.e., what the value of `x` will be at the *current* time step in terms of the *past* value. The output `y`, is given by the value of `x` at the *past* time step, i.e., `y(k) ~ x(k-1)`. If this logic was implemented in an imperative programming style, the logic would thus be
40
51
41
52
```julia
42
53
functiondiscrete_step(x, u)
43
-
y = x # y is assigned the current value of x, y(k) = x(k)
44
-
x =0.5x + u # x is updated to a new value, i.e., x(k+1) is computed
45
-
return x, y # The state x now refers to x at the next time step, x(k+1), while y refers to x at the current time step, y(k) = x(k)
54
+
y = x # y is assigned the current value of x, y(k) = x(k-1)
55
+
x =0.5x + u # x is updated to a new value, i.e., x(k) is computed
56
+
return x, y # The state x now refers to x at the current time step, x(k), while y refers to x at the past time step, y(k) = x(k-1)
46
57
end
47
58
```
48
59
49
60
An alternative and *equivalent* way of writing the same system is
61
+
50
62
```@example clocks
51
63
eqs = [
52
-
x(k) ~ 0.5x(k-1) + u(k-1),
53
-
y(k-1) ~ x(k-1)
64
+
x(k + 1) ~ 0.5x(k) + u(k),
65
+
y(k) ~ x(k)
54
66
]
55
67
```
56
-
Here, we have *shifted all indices* by `-1`, resulting in exactly the same difference equations. However, the next system is *not equivalent* to the previous one:
68
+
69
+
but the use of positive time shifts is not yet supported.
70
+
Instead, we have *shifted all indices* by `-1`, resulting in exactly the same difference equations. However, the next system is *not equivalent* to the previous one:
71
+
57
72
```@example clocks
58
73
eqs = [
59
-
x(k) ~ 0.5x(k-1) + u(k-1),
74
+
x(k) ~ 0.5x(k - 1) + u(k - 1),
60
75
y ~ x
61
76
]
62
77
```
78
+
63
79
In this last example, `y` refers to the updated `x(k)`, i.e., this system is equivalent to
80
+
64
81
```
65
82
eqs = [
66
83
x(k+1) ~ 0.5x(k) + u(k),
@@ -69,47 +86,59 @@ eqs = [
69
86
```
70
87
71
88
## Higher-order shifts
89
+
72
90
The expression `x(k-1)` refers to the value of `x` at the *previous* clock tick. Similarly, `x(k-2)` refers to the value of `x` at the clock tick before that. In general, `x(k-n)` refers to the value of `x` at the `n`th clock tick before the current one. As an example, the Z-domain transfer function
91
+
73
92
```math
74
93
H(z) = \dfrac{b_2 z^2 + b_1 z + b_0}{a_2 z^2 + a_1 z + a_0}
75
94
```
95
+
76
96
may thus be modeled as
97
+
77
98
```julia
78
-
@variables t y(t) [description="Output"] u(t) [description="Input"]
99
+
@variables t y(t) [description="Output"] u(t) [description="Input"]
(see also [ModelingToolkitStandardLibrary](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/) for a discrete-time transfer-function component.)
85
105
106
+
(see also [ModelingToolkitStandardLibrary](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/) for a discrete-time transfer-function component.)
86
107
87
108
## Initial conditions
109
+
88
110
The initial condition of discrete-time variables is defined using the [`ShiftIndex`](@ref) operator, for example
111
+
89
112
```julia
90
113
ODEProblem(model, [x(k) =>1.0], (0.0, 10.0))
91
114
```
115
+
92
116
If higher-order shifts are present, the corresponding initial conditions must be specified, e.g., the presence of the equation
117
+
93
118
```julia
94
-
x(k+1) =x(k) +x(k-1)
119
+
x(k) =x(k-1) +x(k-2)
95
120
```
96
-
requires specification of the initial condition for both `x(k)` and `x(k-1)`.
121
+
122
+
requires specification of the initial condition for both `x(k-1)` and `x(k-2)`.
97
123
98
124
## Multiple clocks
125
+
99
126
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:
127
+
100
128
```julia
101
129
yd1 ~Sample(t, dt1)(y)
102
130
ud1 ~ kp * (Sample(t, dt1)(r) - yd1)
103
131
yd2 ~Sample(t, dt2)(y)
104
132
ud2 ~ kp * (Sample(t, dt2)(r) - yd2)
105
133
```
134
+
106
135
`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.
107
136
108
137
## Accessing discrete-time variables in the solution
109
138
110
-
111
139
## A complete example
112
-
Below, we model a simple continuous first-order system called `plant` that is controlled using a discrete-time controller `controller`. The reference signal is filtered using a discrete-time filter `filt` before being fed to the controller.
140
+
141
+
Below, we model a simple continuous first-order system called `plant` that is controlled using a discrete-time controller `controller`. The reference signal is filtered using a discrete-time filter `filt` before being fed to the controller.
113
142
114
143
```@example clocks
115
144
using ModelingToolkit, Plots, OrdinaryDiffEq
@@ -122,36 +151,35 @@ function plant(; name)
122
151
@variables x(t)=1 u(t)=0 y(t)=0
123
152
D = Differential(t)
124
153
eqs = [D(x) ~ -x + u
125
-
y ~ x]
154
+
y ~ x]
126
155
ODESystem(eqs, t; name = name)
127
156
end
128
157
129
158
function filt(; name) # Reference filter
130
159
@variables x(t)=0 u(t)=0 y(t)=0
131
160
a = 1 / exp(dt)
132
-
eqs = [x(k + 1) ~ a * x + (1 - a) * u(k)
133
-
y ~ x]
161
+
eqs = [x(k) ~ a * x(k - 1) + (1 - a) * u(k)
162
+
y ~ x]
134
163
ODESystem(eqs, t, name = name)
135
164
end
136
165
137
166
function controller(kp; name)
138
167
@variables y(t)=0 r(t)=0 ud(t)=0 yd(t)=0
139
168
@parameters kp = kp
140
169
eqs = [yd ~ Sample(y)
141
-
ud ~ kp * (r - yd)]
170
+
ud ~ kp * (r - yd)]
142
171
ODESystem(eqs, t; name = name)
143
172
end
144
173
145
174
@named f = filt()
146
175
@named c = controller(1)
147
176
@named p = plant()
148
177
149
-
connections = [
150
-
r ~ sin(t) # reference signal
151
-
f.u ~ r # reference to filter input
152
-
f.y ~ c.r # filtered reference to controller reference
153
-
Hold(c.ud) ~ p.u # controller output to plant input (zero-order-hold)
154
-
p.y ~ c.y] # plant output to controller feedback
178
+
connections = [r ~ sin(t) # reference signal
179
+
f.u ~ r # reference to filter input
180
+
f.y ~ c.r # filtered reference to controller reference
181
+
Hold(c.ud) ~ p.u # controller output to plant input (zero-order-hold)
182
+
p.y ~ c.y] # plant output to controller feedback
155
183
156
184
@named cl = ODESystem(connections, t, systems = [f, c, p])
0 commit comments