Skip to content

Commit bca6e28

Browse files
Merge pull request #314 from SebastianM-C/smc/interp
Add `Interpolation` & `ParametrizedInterpolation`
2 parents 1cef2c1 + cfee55c commit bca6e28

File tree

8 files changed

+453
-33
lines changed

8 files changed

+453
-33
lines changed

Project.toml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,45 @@ DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
99
IfElse = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
1010
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1111
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
12+
PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46"
1213
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
1314

1415
[compat]
1516
Aqua = "0.8"
1617
ChainRulesCore = "1.24"
1718
ControlSystemsBase = "1.4"
19+
DataFrames = "1.7"
1820
DataInterpolations = "6"
21+
ForwardDiff = "0.10"
1922
DiffEqBase = "6.152"
2023
IfElse = "0.1"
2124
LinearAlgebra = "1.10"
22-
ModelingToolkit = "9.46.1"
25+
Optimization = "4"
26+
ModelingToolkit = "9.47"
2327
OrdinaryDiffEq = "6.87"
2428
OrdinaryDiffEqDefault = "1.1"
29+
PreallocationTools = "0.4.23"
2530
SafeTestsets = "0.1"
31+
SciMLStructures = "1.4.2"
32+
SymbolicIndexingInterface = "0.3.28"
2633
Symbolics = "6.14"
2734
Test = "1"
2835
julia = "1.10"
2936

3037
[extras]
3138
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
3239
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
40+
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
3341
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
42+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
3443
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
44+
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
3545
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
3646
OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7"
3747
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
48+
SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226"
49+
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
3850
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3951

4052
[targets]
41-
test = ["Aqua", "LinearAlgebra", "OrdinaryDiffEq", "OrdinaryDiffEqDefault", "SafeTestsets", "Test", "ControlSystemsBase", "DataInterpolations"]
53+
test = ["Aqua", "LinearAlgebra", "OrdinaryDiffEqDefault", "OrdinaryDiffEq", "Optimization", "SafeTestsets", "Test", "ControlSystemsBase", "DataFrames", "DataInterpolations", "SciMLStructures", "SymbolicIndexingInterface", "ForwardDiff"]

docs/Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[deps]
22
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
3+
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
4+
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
35
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
46
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
57
IfElse = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
@@ -10,6 +12,8 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1012

1113
[compat]
1214
ControlSystemsBase = "1.1"
15+
DataFrames = "1.7"
16+
DataInterpolations = "6.4"
1317
DifferentialEquations = "7.6"
1418
Documenter = "1"
1519
IfElse = "0.1"

docs/src/tutorials/dc_motor_pi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ so that it can be represented as a system of `ODEs` (ordinary differential equat
7777

7878
```@example dc_motor_pi
7979
sys = structural_simplify(model)
80-
prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0, 6.0))
80+
prob = ODEProblem(sys, [sys.L1.i => 0.0], (0, 6.0))
8181
sol = solve(prob)
8282
8383
p1 = plot(sol.t, sol[sys.inertia.w], ylabel = "Angular Vel. in rad/s",

docs/src/tutorials/input_component.md

Lines changed: 149 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,171 @@
1-
# Running Models with Discrete Data
1+
# Building Models with Discrete Data, Interpolations, and Lookup Tables
22

3-
There are 3 ways to include data as part of a model.
3+
There are 4 ways to include data as part of a model.
44

5-
1. using `ModelingToolkitStandardLibrary.Blocks.TimeVaryingFunction`
6-
2. using a custom component with external data
7-
3. using `ModelingToolkitStandardLibrary.Blocks.SampledData`
5+
1. using `ModelingToolkitStandardLibrary.Blocks.Interpolation`
6+
2. using `ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation`
7+
3. using a custom component with external data (not recommended)
8+
4. using `ModelingToolkitStandardLibrary.Blocks.SampledData` (legacy)
89

910
This tutorial demonstrate each case and explain the pros and cons of each.
1011

11-
## `TimeVaryingFunction` Component
12+
## `Interpolation` Block
1213

13-
The `ModelingToolkitStandardLibrary.Blocks.TimeVaryingFunction` component is easy to use and is performant. However the data is locked to the `ODESystem` and can only be changed by building a new `ODESystem`. Therefore, running a batch of data would not be efficient. Below is an example of how to use the `TimeVaryingFunction` with `DataInterpolations` to build the function from sampled discrete data.
14+
The `ModelingToolkitStandardLibrary.Blocks.Interpolation` component is easy to use and is performant.
15+
It is similar to using callable parameters, but it provides a block interface with `RealInput` and `RealOutput` connectors.
16+
The `Interpolation` is compatible with interpolation types from `DataInterpolation`.
1417

15-
```julia
18+
```@docs
19+
ModelingToolkitStandardLibrary.Blocks.Interpolation
20+
```
21+
22+
Here is an example on how to use it. Let's consider a mass-spring-damper system, where
23+
we have an external force as an input. We then generate some example data in a `DataFrame`
24+
that would represent a measurement of the input. In a more realistic case, this `DataFrame`
25+
would be read from a file.
26+
27+
```@example interpolation_block
1628
using ModelingToolkit
1729
using ModelingToolkit: t_nounits as t, D_nounits as D
1830
using ModelingToolkitStandardLibrary.Blocks
1931
using DataInterpolations
2032
using OrdinaryDiffEq
33+
using DataFrames
34+
using Plots
2135
22-
function System(f; name)
23-
src = TimeVaryingFunction(f)
36+
function MassSpringDamper(; name)
37+
@named input = RealInput()
38+
@variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0
39+
@parameters m=10 k=1000 d=1
2440
25-
vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0
41+
eqs = [
42+
f ~ input.u
43+
ddx * 10 ~ k * x + d * dx + f
44+
D(x) ~ dx
45+
D(dx) ~ ddx]
46+
47+
ODESystem(eqs, t; name, systems = [input])
48+
end
49+
50+
function MassSpringDamperSystem(data, time; name)
51+
@named src = Interpolation(LinearInterpolation, data, time)
52+
@named clk = ContinuousClock()
53+
@named model = MassSpringDamper()
54+
55+
eqs = [
56+
connect(src.input, clk.output)
57+
connect(src.output, model.input)
58+
]
59+
60+
ODESystem(eqs, t, [], []; name, systems = [src, clk, model])
61+
end
62+
63+
function generate_data()
64+
dt = 4e-4
65+
time = 0:dt:0.1
66+
data = sin.(2 * pi * time * 100)
67+
68+
return DataFrame(; time, data)
69+
end
70+
71+
df = generate_data() # example data
72+
73+
@named system = MassSpringDamperSystem(df.data, df.time)
74+
sys = structural_simplify(system)
75+
prob = ODEProblem(sys, [], (0, df.time[end]))
76+
sol = solve(prob)
77+
plot(sol)
78+
```
79+
80+
## `ParametrizedInterpolation` Block
81+
82+
The `ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation` component is similar to `Interpolation`, but as the name suggests, it is parametrized by the data, allowing one to change the underlying data without rebuilding the model as the data is represented via vector parameters.
83+
The main advantage of this block over the [`Interpolation`](@ref) one is that one can use it for optimization problems. Currently, this supports forward mode AD via ForwardDiff, but due to the increased flexibility of the types in the component, this is not as fast as the `Interpolation` block,
84+
so it is recommended to use only when the added flexibility is required.
85+
86+
```@docs
87+
ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation
88+
```
89+
90+
Here is an example on how to use it
91+
92+
```@example parametrized_interpolation
93+
using ModelingToolkit
94+
using ModelingToolkit: t_nounits as t, D_nounits as D
95+
using ModelingToolkitStandardLibrary.Blocks
96+
using DataInterpolations
97+
using OrdinaryDiffEq
98+
using DataFrames
99+
using Plots
100+
101+
function MassSpringDamper(; name)
102+
@named input = RealInput()
103+
vars = @variables f(t) x(t)=0 dx(t) [guess=0] ddx(t)
26104
pars = @parameters m=10 k=1000 d=1
27105
28-
eqs = [f ~ src.output.u
106+
eqs = [
107+
f ~ input.u
29108
ddx * 10 ~ k * x + d * dx + f
30109
D(x) ~ dx
31110
D(dx) ~ ddx]
32111
33-
ODESystem(eqs, t, vars, pars; systems = [src], name)
112+
ODESystem(eqs, t, vars, pars; name, systems = [input])
34113
end
35114
36-
dt = 4e-4
37-
time = 0:dt:0.1
38-
data = sin.(2 * pi * time * 100) # example data
115+
function MassSpringDamperSystem(data, time; name)
116+
@named src = ParametrizedInterpolation(LinearInterpolation, data, time)
117+
@named clk = ContinuousClock()
118+
@named model = MassSpringDamper()
119+
120+
eqs = [
121+
connect(model.input, src.output)
122+
connect(src.input, clk.output)
123+
]
39124
40-
f = LinearInterpolation(data, time)
125+
ODESystem(eqs, t; name, systems = [src, clk, model])
126+
end
127+
128+
function generate_data()
129+
dt = 4e-4
130+
time = 0:dt:0.1
131+
data = sin.(2 * pi * time * 100)
132+
133+
return DataFrame(; time, data)
134+
end
41135
42-
@named system = System(f)
136+
df = generate_data() # example data
137+
138+
@named system = MassSpringDamperSystem(df.data, df.time)
43139
sys = structural_simplify(system)
44-
prob = ODEProblem(sys, [], (0, time[end]))
45-
sol = solve(prob, ImplicitEuler())
140+
prob = ODEProblem(sys, [], (0, df.time[end]))
141+
sol = solve(prob)
142+
plot(sol)
143+
```
144+
145+
If we want to run a new data set, this requires only remaking the problem and solving again
146+
```@example parametrized_interpolation
147+
prob2 = remake(prob, p = [sys.src.data => ones(length(df.data))])
148+
sol2 = solve(prob2)
149+
plot(sol2)
46150
```
47151

48-
If we want to run a new data set, this requires building a new `LinearInterpolation` and `ODESystem` followed by running `structural_simplify`, all of which takes time. Therefore, to run several pieces of data it's better to re-use an `ODESystem`. The next couple methods will demonstrate how to do this.
152+
!!! note
153+
Note that when changing the data, the length of the new data must be the same as the length of the original data.
49154

50155
## Custom Component with External Data
51156

52157
The below code shows how to include data using a `Ref` and registered `get_sampled_data` function. This example uses a very basic function which requires non-adaptive solving and sampled data. As can be seen, the data can easily be set and changed before solving.
53158

54-
```julia
159+
```@example custom_component_external_data
160+
using ModelingToolkit
161+
using ModelingToolkit: t_nounits as t, D_nounits as D
162+
using ModelingToolkitStandardLibrary.Blocks
163+
using OrdinaryDiffEq
164+
55165
const rdata = Ref{Vector{Float64}}()
56166
167+
dt = 4e-4
168+
time = 0:dt:0.1
57169
# Data Sets
58170
data1 = sin.(2 * pi * time * 100)
59171
data2 = cos.(2 * pi * time * 50)
@@ -106,8 +218,13 @@ Additional code could be added to resolve this issue, for example by using a `Re
106218
To resolve the issues presented above, the `ModelingToolkitStandardLibrary.Blocks.SampledData` component can be used which allows for a resusable `ODESystem` and self contained data which ensures a solution which remains valid for it's lifetime. Now it's possible to also parallelize the call to `solve()`.
107219

108220
```julia
221+
using ModelingToolkit
222+
using ModelingToolkit: t_nounits as t, D_nounits as D
223+
using ModelingToolkitStandardLibrary.Blocks
224+
using OrdinaryDiffEq
225+
109226
function System(; name)
110-
src = SampledData(Float64)
227+
src = SampledData(Float64, name=:src)
111228

112229
vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0
113230
pars = @parameters m=10 k=1000 d=1
@@ -121,16 +238,22 @@ function System(; name)
121238
end
122239

123240
@named system = System()
124-
sys = structural_simplify(system)
241+
sys = structural_simplify(system, split=false)
125242
s = complete(system)
126-
prob = ODEProblem(sys, [], (0, time[end]); tofloat = false)
243+
244+
dt = 4e-4
245+
time = 0:dt:0.1
246+
data1 = sin.(2 * pi * time * 100)
247+
data2 = cos.(2 * pi * time * 50)
248+
249+
prob = ODEProblem(sys, [], (0, time[end]); split=false, tofloat = false, use_union=true)
127250
defs = ModelingToolkit.defaults(sys)
128251

129252
function get_prob(data)
130253
defs[s.src.buffer] = Parameter(data, dt)
131254
# ensure p is a uniform type of Vector{Parameter{Float64}} (converting from Vector{Any})
132255
p = Parameter.(ModelingToolkit.varmap_to_vars(defs, parameters(sys); tofloat = false))
133-
remake(prob; p)
256+
remake(prob; p, build_initializeprob=false)
134257
end
135258

136259
prob1 = get_prob(data1)

src/Blocks/Blocks.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export Log, Log10
1616
include("math.jl")
1717

1818
export Constant, TimeVaryingFunction, Sine, Cosine, ContinuousClock, Ramp, Step, ExpSine,
19-
Square, Triangular, Parameter, SampledData
19+
Square, Triangular, Parameter, SampledData,
20+
Interpolation, ParametrizedInterpolation
2021
include("sources.jl")
2122

2223
export Limiter, DeadZone, SlewRateLimiter

0 commit comments

Comments
 (0)