Skip to content

Commit 4f90e60

Browse files
authored
Merge pull request #165 from SciML/bgc/input_fix
Input changed to SampledData
2 parents a3213be + e62e80f commit 4f90e60

File tree

14 files changed

+386
-206
lines changed

14 files changed

+386
-206
lines changed

docs/pages.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pages = [
55
"Custom Components" => "tutorials/custom_component.md",
66
"Thermal Conduction Model" => "tutorials/thermal_model.md",
77
"DC Motor with Speed Controller" => "tutorials/dc_motor_pi.md",
8-
"Input Component" => "tutorials/input_component.md",
8+
"SampledData Component" => "tutorials/input_component.md",
99
],
1010
"About Acausal Connections" => "connectors/connections.md",
1111
"API" => [

docs/src/tutorials/input_component.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Running Models with Discrete Data
22

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

55
1. using `ModelingToolkitStandardLibrary.Blocks.TimeVaryingFunction`
6-
2. using a custom component with external data or sensor
7-
3. using `ModelingToolkitStandardLibrary.Blocks.Input`
6+
2. using a custom component with external data
7+
3. using `ModelingToolkitStandardLibrary.Blocks.SampledData`
88

99
This tutorial demonstrate each case and explain the pros and cons of each.
1010

11-
## TimeVaryingFunction Component
11+
## `TimeVaryingFunction` Component
1212

13-
This component is easy to use and is performative. 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 discrete data.
13+
The `ModelingToolkitStandardLibrary.Blocks.TimeVaryingFunction` component is easy to use and is performative. 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.
1414

1515
```julia
1616
using ModelingToolkit
@@ -47,11 +47,11 @@ prob = ODEProblem(sys, [], (0, time[end]))
4747
sol = solve(prob, ImplicitEuler())
4848
```
4949

50-
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 serveral pieces of data it's better to re-use an `ODESystem`. The next couple methods will demonstrate this.
50+
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 serveral pieces of data it's better to re-use an `ODESystem`. The next couple methods will demonstrate how to do this.
5151

5252
## Custom Component with External Data
5353

54-
The below code shows how to include data using a `Ref` and registered `input` 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.
54+
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.
5555

5656
```julia
5757
const rdata = Ref{Vector{Float64}}()
@@ -60,20 +60,20 @@ const rdata = Ref{Vector{Float64}}()
6060
data1 = sin.(2 * pi * time * 100)
6161
data2 = cos.(2 * pi * time * 50)
6262

63-
function input(t)
63+
function get_sampled_data(t)
6464
i = floor(Int, t / dt) + 1
6565
x = rdata[][i]
6666

6767
return x
6868
end
6969

70-
Symbolics.@register_symbolic input(t)
70+
Symbolics.@register_symbolic get_sampled_data(t)
7171

7272
function System(; name)
7373
vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0
7474
pars = @parameters m=10 k=1000 d=1
7575

76-
eqs = [f ~ input(t)
76+
eqs = [f ~ get_sampled_data(t)
7777
ddx * 10 ~ k * x + d * dx + f
7878
D(x) ~ dx
7979
D(dx) ~ ddx]
@@ -94,20 +94,22 @@ sol2 = solve(prob, ImplicitEuler(); dt, adaptive = false)
9494
ddx2 = sol2[sys.ddx]
9595
```
9696

97-
The drawback of this method is that the solution observables can be linked to the data `Ref`, which means that if the data changes then the observables are no longer valid. In this case `ddx` is an observable that is derived directly from the data. Therefore, `sol1[sys.ddx]` is no longer correct after the data is changed for `sol2`. Additional code could be added to resolve this issue, for example by using a `Ref{Dict}` that could link a parameter of the model to the data source. This would also be necessary for parallel processing.
97+
The drawback of this method is that the solution observables can be linked to the data `Ref`, which means that if the data changes then the observables are no longer valid. In this case `ddx` is an observable that is derived directly from the data. Therefore, `sol1[sys.ddx]` is no longer correct after the data is changed for `sol2`.
9898

9999
```julia
100100
# the following test will fail
101101
@test all(ddx1 .== sol1[sys.ddx]) #returns false
102102
```
103103

104-
## Input Component
104+
Additional code could be added to resolve this issue, for example by using a `Ref{Dict}` that could link a parameter of the model to the data source. This would also be necessary for parallel processing.
105105

106-
To resolve the issues presented above, the `Input` 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 parallize the solving.
106+
## `SampledData` Component
107+
108+
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 parallize the call to `solve()`.
107109

108110
```julia
109111
function System(; name)
110-
src = Input(Float64)
112+
src = SampledData(Float64)
111113

112114
vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0
113115
pars = @parameters m=10 k=1000 d=1
@@ -144,4 +146,4 @@ sol2 = Ref{ODESolution}()
144146
end
145147
```
146148

147-
Note, in the above example, we can build the system with an empty `Input` component, only setting the expected data type: `@named src = Input(Float64)`. It's also possible to initialize the component with real data: `@named src = Input(data, dt)`. Additionally note that before running an `ODEProblem` using the `Input` component that the parameter vector should be a uniform type so that Julia is not slowed down by type instability. Because the `Input` component contains the `buffer` parameter of type `Parameter`, we must generate the problem using `tofloat=false`. This will initially give a parameter vector of type `Vector{Any}` with a mix of numbers and `Parameter` type. We can convert the vector to all `Parameter` type by running `p = Parameter.(p)`. This will wrap all the single values in a `Parameter` type which will be mathmatically equivalent to a `Number`.
149+
Note, in the above example, we can build the system with an empty `SampledData` component, only setting the expected data type: `@named src = SampledData(Float64)`. It's also possible to initialize the component with real sampled data: `@named src = SampledData(data, dt)`. Additionally note that before running an `ODEProblem` using the `SampledData` component, one must be careful about the parameter vector Type. The `SampledData` component contains a `buffer` parameter of type `Parameter`, therefore we must generate the problem using `tofloat=false`. This will initially give a parameter vector of type `Vector{Any}` with a mix of numbers and `Parameter` type. We can convert the vector to a uniform `Parameter` type by running `p = Parameter.(p)`. This will wrap all the single values in a `Parameter` which will be mathmatically equivalent to a `Number`.

src/Blocks/Blocks.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export Log, Log10
1818
include("math.jl")
1919

2020
export Constant, TimeVaryingFunction, Sine, Cosine, ContinuousClock, Ramp, Step, ExpSine,
21-
Square, Triangular, Parameter, Input
21+
Square, Triangular, Parameter, SampledData
2222
include("sources.jl")
2323

2424
export Limiter, DeadZone, SlewRateLimiter

src/Blocks/sources.jl

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -488,18 +488,14 @@ function Parameter(x::T; tofloat = true) where {T <: Real}
488488
end
489489
Parameter(x::Vector{T}, dt::T) where {T <: Real} = Parameter(x, dt, length(x))
490490

491-
function input(t, memory::Parameter{T}) where {T}
491+
function get_sampled_data(t, memory::Parameter{T}) where {T}
492492
if t < 0
493493
t = zero(t)
494494
end
495495

496496
if isempty(memory.data)
497-
if T isa Float16
498-
return NaN16
499-
elseif T isa Float32
500-
return NaN32
501-
elseif T isa Float64
502-
return NaN64
497+
if T <: AbstractFloat
498+
return T(NaN)
503499
else
504500
return zero(T)
505501
end
@@ -528,27 +524,27 @@ end
528524
get_sample_time(memory::Parameter) = memory.ref
529525
Symbolics.@register_symbolic get_sample_time(memory)
530526

531-
Symbolics.@register_symbolic input(t, memory)
527+
Symbolics.@register_symbolic get_sampled_data(t, memory)
532528

533529
function first_order_backwards_difference(t, memory)
534530
Δt = get_sample_time(memory)
535-
x1 = input(t, memory)
536-
x0 = input(t - Δt, memory)
531+
x1 = get_sampled_data(t, memory)
532+
x0 = get_sampled_data(t - Δt, memory)
537533

538534
return (x1 - x0) / Δt
539535
end
540536

541-
function Symbolics.derivative(::typeof(input), args::NTuple{2, Any}, ::Val{1})
537+
function Symbolics.derivative(::typeof(get_sampled_data), args::NTuple{2, Any}, ::Val{1})
542538
first_order_backwards_difference(args[1], args[2])
543539
end
544540

545-
Input(T::Type; name) = Input(T[], zero(T); name)
546-
function Input(data::Vector{T}, dt::T; name) where {T <: Real}
547-
Input(; name, buffer = Parameter(data, dt))
541+
SampledData(T::Type; name) = SampledData(T[], zero(T); name)
542+
function SampledData(data::Vector{T}, dt::T; name) where {T <: Real}
543+
SampledData(; name, buffer = Parameter(data, dt))
548544
end
549545

550546
"""
551-
Input(; name, buffer)
547+
SampledData(; name, buffer)
552548
553549
data input component.
554550
@@ -558,13 +554,14 @@ data input component.
558554
# Connectors:
559555
- `output`
560556
"""
561-
@component function Input(; name, buffer)
557+
@component function SampledData(; name, buffer)
562558
pars = @parameters begin buffer = buffer end
563559
vars = []
564560
systems = @named begin output = RealOutput() end
565561
eqs = [
566-
output.u ~ input(t, buffer),
562+
output.u ~ get_sampled_data(t, buffer),
567563
]
568564
return ODESystem(eqs, t, vars, pars; name, systems,
569-
defaults = [output.u => input(0.0, buffer)]) #TODO: get initial value from buffer
565+
defaults = [output.u => get_sampled_data(0.0, buffer)])
570566
end
567+
@deprecate Input SampledData

src/Hydraulic/IsothermalCompressible/IsothermalCompressible.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ D = Differential(t)
1616
export HydraulicPort, HydraulicFluid
1717
include("utils.jl")
1818

19-
export Source, InputSource, Cap, Tube, FixedVolume, DynamicVolume
19+
export Cap, Tube, FixedVolume, DynamicVolume
2020
include("components.jl")
2121

22+
export MassFlow, Pressure, FixedPressure
23+
include("sources.jl")
24+
2225
end

0 commit comments

Comments
 (0)