Defining New Component Types
Causal provides a library that includes some well-known components that are ready to be used. For example,
FunctionGenerator
,SinewaveGenerator
,SquarewaveGenerator
,RampGenerator
, etc. as sourcesStaticSystem
,Adder
,Multiplier
,Gain
, etc. as static systemsDiscreteSystem
,DiscreteLinearSystem
,HenonSystem
,LogisticSystem
, etc. as dynamical systems represented by discrete difference equations.ODESystem
,LorenzSystem
,ChenSystem
,ChuaSystem
, etc. as dynamical systems represented by ODEs.DAESystem
,RobertsonSystem
, etc. as dynamical systems represented by dynamical systems represented by DAEs.RODESystem
,MultiplicativeNoiseLinearSystem
, etc. as dynamical systems represented by dynamical systems represented by RODEs.SDESystem
,NoisyLorenzSystem
,ForcedNoisyLorenzSystem
, etc. as dynamical systems represented by dynamical systems represented by SDEs.DDESystem
,DelayFeedbackSystem
, etc. as dynamical systems represented by dynamical systems represented by DDEs.Writer
,Printer
,Scope
, etc. as sinks.
It is very natural that this library may lack some of the components that are wanted to be used by the user. In such a case, Causal provides the users with the flexibility to enrich this library. The users can define their new component types, including source, static system, dynamical system, sink and use them with ease.
Defining A New Source
New source types are defines using @def_source
macro. Before embarking on defining new source, let us get the necessary information on how to use @def_source
. This can be can be obtained through its docstrings.
julia> using Causal # hide
julia> @doc @def_source
@def_source ex
where ex is the expression to define to define a new AbstractSource
component type. The usage is as follows:
@def_source struct MySource{T1,T2,T3,...,TN,OP, RO} <: AbstractSource
param1::T1 = param1_default # optional field
param2::T2 = param2_default # optional field
param3::T3 = param3_default # optional field
⋮
paramN::TN = paramN_default # optional field
output::OP = output_default # mandatory field
readout::RO = readout_function # mandatory field
end
Here, MySource has N parameters, an output port and a readout function.
│ Warning
│
│ output and readout are mandatory fields to define a new source.
│ The rest of the fields are the parameters of the source.
│ Warning
│
│ readout must be a single-argument function, i.e. a function of
│ time t.
│ Warning
│
│ New source must be a subtype of AbstractSource to function
│ properly.
Example
≡≡≡≡≡≡≡≡≡
julia> @def_source struct MySource{OP, RO} <: AbstractSource
a::Int = 1
b::Float64 = 2.
output::OP = Outport()
readout::RO = t -> (a + b) * sin(t)
end
julia> gen = MySource();
julia> gen.a
1
julia> gen.output
1-element Outport{Outpin{Float64}}:
Outpin(eltype:Float64, isbound:false)
From the docstring, its clear that new types of source can be defined as if we define a new Julia type. The difference is that the struct
keyword is preceded by @def_source
macro and the new component must be a subtype of AbstractSource
. Also from the docstring is that the new type has some optional and mandatory fields.
To define a new source, mandatory fields must be defined. The optional fields are the parameters of the source.
For example let us define a new source that generates waveforms of the form.
\[ y(t) = \begin{bmatrix} \alpha sin(t) \\ \beta cos(t) \end{bmatrix}\]
Here $\alpha$ and $beta$ is the system parameters. That is, while defining the new source component, $\alpha$ and $\beta$ are optional fields. readout
and $output$ are the mandatory field while defining a source. Note from above equation that the output of the new source has two pins. Thus, this new source component type, say MySource
is defined as follows.
julia> @def_source struct MySource{RO, OP} <: AbstractSource
α::Float64 = 1.
β::Float64 = 2.
readout::RO = (t, α=α, β=β) -> [α*sin(t), β*cos(t)]
output::OP = Outport(2)
end
Note that the syntax is very similar to the case in which we define a normal Julia type. We start with struct
keyword preceded with @def_source
macro. In order for the MySource
to work flawlessly, i.e. to be used a model component, it must a subtype of AbstractSource
. The readout
function of MySource
is a function of t
and the remaining parameters, i.e., $\alpha$ and $\beta$, are passed into as optional arguments to avoid global variables.
One other important point to note is that the MySource
has additional fields that are required for it to work as a regular model component. Let us print all the field names of MySource
,
julia> fieldnames(MySource)
(:α, :β, :readout, :output, :trigger, :handshake, :callbacks, :name, :id)
We know that we defined the fields α, β, readout, output
, but, the fields trigger, callback, handshake, callbacks, name, id
are defined automatically by @def_source
macro.
Since the type MySource
has been defined, any instance of it can be constructed. Let us see the constructors first.
julia> methods(MySource)
# 2 methods for type constructor:
[1] (::Type{Main.ex-defining_new_components_ex.MySource})(; α, β, readout, output, trigger, handshake, callbacks, name, id) in Main.ex-defining_new_components_ex at util.jl:448
[2] (::Type{Main.ex-defining_new_components_ex.MySource})(α::Float64, β::Float64, readout::RO, output::OP, trigger::var"##253", handshake::var"##254", callbacks::var"##255", name::Symbol, id::var"##256") where {RO, OP, var"##253", var"##254", var"##255", Symbol, var"##256"} in Main.ex-defining_new_components_ex at none:2
The constructor with the keyword arguments is very much easy to uses.
julia> gen1 = MySource()
Main.ex-defining_new_components_ex.MySource{Main.ex-defining_new_components_ex.var"#2#7"{Float64,Float64},Outport{Outpin{Float64}},Inpin{Float64},Outpin{Bool},Nothing,Symbol,Base.UUID}(1.0, 2.0, Main.ex-defining_new_components_ex.var"#2#7"{Float64,Float64}(1.0, 2.0), Outport(numpins:2, eltype:Outpin{Float64}), Inpin(eltype:Float64, isbound:false), Outpin(eltype:Bool, isbound:false), nothing, Symbol(""), UUID("30be27b0-7563-4712-9e87-bd88b3eece83"))
julia> gen2 = MySource(α=4.)
Main.ex-defining_new_components_ex.MySource{Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64},Outport{Outpin{Float64}},Inpin{Float64},Outpin{Bool},Nothing,Symbol,Base.UUID}(4.0, 2.0, Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64}(4.0, 2.0), Outport(numpins:2, eltype:Outpin{Float64}), Inpin(eltype:Float64, isbound:false), Outpin(eltype:Bool, isbound:false), nothing, Symbol(""), UUID("15aee6a2-bff9-48d9-b6ad-b496be087e31"))
julia> gen3 = MySource(α=4., β=5.)
Main.ex-defining_new_components_ex.MySource{Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64},Outport{Outpin{Float64}},Inpin{Float64},Outpin{Bool},Nothing,Symbol,Base.UUID}(4.0, 5.0, Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64}(4.0, 5.0), Outport(numpins:2, eltype:Outpin{Float64}), Inpin(eltype:Float64, isbound:false), Outpin(eltype:Bool, isbound:false), nothing, Symbol(""), UUID("12a2ba45-2d64-4156-bf0a-a54b5eabe543"))
julia> gen3 = MySource(α=4., β=5., name=:mygen)
Main.ex-defining_new_components_ex.MySource{Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64},Outport{Outpin{Float64}},Inpin{Float64},Outpin{Bool},Nothing,Symbol,Base.UUID}(4.0, 5.0, Main.ex-defining_new_components_ex.var"#3#8"{Float64,Float64}(4.0, 5.0), Outport(numpins:2, eltype:Outpin{Float64}), Inpin(eltype:Float64, isbound:false), Outpin(eltype:Bool, isbound:false), nothing, :mygen, UUID("a737b994-5f0b-4670-9743-a4a21fcd6690"))
julia> gen3.trigger
Inpin(eltype:Float64, isbound:false)
julia> gen3.id
UUID("a737b994-5f0b-4670-9743-a4a21fcd6690")
julia> gen3.α
4.0
julia> gen3.β
5.0
julia> gen3.output
2-element Outport{Outpin{Float64}}:
Outpin(eltype:Float64, isbound:false)
Outpin(eltype:Float64, isbound:false)
An instance works flawlessly as a model component, that is, it can be driven from its trigger
pin and signalling cane be carried out from its handshake
pin. To see this, let us construct required pins and ports to drive a MySource
instance.
julia> gen = MySource() # `MySource` instance
Main.ex-defining_new_components_ex.MySource{Main.ex-defining_new_components_ex.var"#2#7"{Float64,Float64},Outport{Outpin{Float64}},Inpin{Float64},Outpin{Bool},Nothing,Symbol,Base.UUID}(1.0, 2.0, Main.ex-defining_new_components_ex.var"#2#7"{Float64,Float64}(1.0, 2.0), Outport(numpins:2, eltype:Outpin{Float64}), Inpin(eltype:Float64, isbound:false), Outpin(eltype:Bool, isbound:false), nothing, Symbol(""), UUID("4f6e6804-d94a-4c5e-a4a9-dc7251fbcfde"))
julia> trg = Outpin() # To trigger `gen`
Outpin(eltype:Float64, isbound:false)
julia> hnd = Inpin{Bool}() # To signalling with `gen`
Inpin(eltype:Bool, isbound:false)
julia> iport = Inport(2) # To take values out of `gen`
2-element Inport{Inpin{Float64}}:
Inpin(eltype:Float64, isbound:false)
Inpin(eltype:Float64, isbound:false)
julia> connect!(trg, gen.trigger);
julia> connect!(gen.handshake, hnd);
julia> connect!(gen.output, iport);
julia> launch(gen) # Launch `gen,
Task (runnable) @0x00007f1fd0878010
Now gen
can be driven through trg
pin.
julia> put!(trg, 1.) # Drive `gen` for `t=1`.
julia> take!(iport) # Read output of `gen` from `iport`
2-element Array{Float64,1}:
0.8414709848078965
1.0806046117362795
julia> take!(hnd) # Approve `gen` has taken a step.
true
Thus, by using @def_source
macro, it is possible for the users to define any type of sources under AbstractSource
type and user them without a hassle.
The procedure is again the same for any other component types. The table below lists the macros that are used to define new component types.
Macro | Component Type | Supertype | Mandatory Field Names |
---|---|---|---|
@def_source | Source | AbstractSource | readout , output |
@def_static_system | StaticSystem | AbstractStaticSystem | readout , output , input |
@def_discrete_system | Discrete Dynamic System | AbstractDiscreteSystem | righthandside , readout , state , input , output |
@def_ode_system | ODE Dynamic System | AbstractODESystem | righthandside , readout , state , input , output |
@def_dae_system | DAE Dynamic System | AbstractDAESystem | righthandside , readout , state , stateder , diffvars , input , output |
@def_rode_system | RODE Dynamic System | AbstractRODESystem | righthandside , readout , state , input , output |
@def_sde_system | SDE Dynamic System | AbstractSDESystem | drift , diffusion , readout , state , input , output |
@def_dde_system | DDE Dynamic System | AbstractDDESystem | constlags , depslags , righthandside , history , readout , state , input , output |
@def_sink | Sink | AbstractSink | action |
The steps followed in the previous section are the same to define other component types: start with suitable macro given above, make the newly-defined type a subtype of the corresponding supertype, define the optional fields (if exist) ands define the mandatory fields of the new type (with the default values if necessary).
Defining New StaticSystem
Consider the following readout function of the static system to be defined
\[y = u_1 t + a cos(u_2)\]
where $u = [u_1, u_2]$ is the input, $y$ is the output of the system and $t$ is time. The system has two inputs and one output. This system can be defined as follows.
julia> @def_static_system struct MyStaticSystem{RO, IP, OP} <: AbstractStaticSystem
a::Float64 = 1.
readout::RO = (t, a = a) -> u[1] * t + a * cos(u[2])
input::IP = Inport(2)
output::OP = Outport(1)
end
Defining New Discrete Dynamical System
The discrete dynamical system given by
\[\begin{array}{l} x_{k + 1} = α x_k + u_k \\[0.25cm] y_k = x_k \end{array}\]
can be defined as,
julia> @def_discrete_system struct MyDiscreteSystem{RH, RO, IP, OP} <: AbstractDiscreteSystem
α::Float64 = 1.
β::Float64 = 2.
righthandside::RH = (dx, x, u, t, α=α) -> (dx[1] = α * x[1] + u[1](t))
readout::RO = (x, u, t) -> x
input::IP = Inport(1)
output::OP = Outport(1)
end
ERROR: LoadError: Invalid usage. The expression should start with `mutable struct`.
struct MyDiscreteSystem{RH, RO, IP, OP} <: AbstractDiscreteSystem
#= none:2 =#
α::Float64 = 1.0
#= none:3 =#
β::Float64 = 2.0
#= none:4 =#
righthandside::RH = ((dx, x, u, t, α = α)->begin
#= none:4 =#
dx[1] = α * x[1] + (u[1])(t)
end)
#= none:5 =#
readout::RO = ((x, u, t)->begin
#= none:5 =#
x
end)
#= none:6 =#
input::IP = Inport(1)
#= none:7 =#
output::OP = Outport(1)
end
in expression starting at none:1
Defining New ODE Dynamical System
The ODE dynamical system given by
\[\begin{array}{l} \dot{x} = α x + u \\[0.25cm] y = x \end{array}\]
can be defined as,
julia> @def_ode_system struct MyODESystem{RH, RO, IP, OP} <: AbstractDiscreteSystem
α::Float64 = 1.
β::Float64 = 2.
righthandside::RH = (dx, x, u, t, α=α) -> (dx[1] = α * x[1] + u[1](t))
readout::RO = (x, u, t) -> x
input::IP = Inport(1)
output::OP = Outport(1)
end
ERROR: LoadError: Invalid usage. The expression should start with `mutable struct`.
struct MyODESystem{RH, RO, IP, OP} <: AbstractDiscreteSystem
#= none:2 =#
α::Float64 = 1.0
#= none:3 =#
β::Float64 = 2.0
#= none:4 =#
righthandside::RH = ((dx, x, u, t, α = α)->begin
#= none:4 =#
dx[1] = α * x[1] + (u[1])(t)
end)
#= none:5 =#
readout::RO = ((x, u, t)->begin
#= none:5 =#
x
end)
#= none:6 =#
input::IP = Inport(1)
#= none:7 =#
output::OP = Outport(1)
end
in expression starting at none:1
Defining New DAE Dynamical System
The DAE dynamical system given by
\[\begin{array} dx = x + 1 \\[0.25cm] 0 = 2(x + 1) + 2 \end{array}\]
can be defined as,
julia> @def_dae_system mutable struct MyDAESystem{RH, RO, ST, IP, OP} <: AbstractDAESystem
righthandside::RH = function sfuncdae(out, dx, x, u, t)
out[1] = x[1] + 1 - dx[1]
out[2] = (x[1] + 1) * x[2] + 2
end
readout::RO = (x,u,t) -> x
state::ST = [1., -1]
stateder::ST = [2., 0]
diffvars::Vector{Bool} = [true, false]
input::IP = nothing
output::OP = Outport(1)
end
Defining RODE Dynamical System
The RODE dynamical system given by
\[\begin{array}{l} \dot{x} = A x W \\[0.25cm] y = x \end{array}\]
where
\[A = \begin{bmatrix} 2 & 0 \\ 0 & -2 \end{bmatrix}\]
can be defined as,
julia> @def_rode_system struct MyRODESystem{RH, RO, IP, OP} <: AbstractRODESystem
A::Matrix{Float64} = [2. 0.; 0 -2]
righthandside::RH = (dx, x, u, t, W) -> (dx .= A * x * W)
readout::RO = (x, u, t) -> x
state::Vector{Float64} = rand(2)
input::IP = nothing
output::OP = Outport(2)
end
ERROR: LoadError: Invalid usage. The expression should start with `mutable struct`.
struct MyRODESystem{RH, RO, IP, OP} <: AbstractRODESystem
#= none:2 =#
A::Matrix{Float64} = [2.0 0.0; 0 -2]
#= none:3 =#
righthandside::RH = ((dx, x, u, t, W)->begin
#= none:3 =#
dx .= A * x * W
end)
#= none:4 =#
readout::RO = ((x, u, t)->begin
#= none:4 =#
x
end)
#= none:5 =#
state::Vector{Float64} = rand(2)
#= none:6 =#
input::IP = nothing
#= none:7 =#
output::OP = Outport(2)
end
in expression starting at none:1
Defining SDE Dynamical System
The RODE dynamical system given by
\[\begin{array}{l} dx = -x dt + dW \\[0.25cm] y = x \end{array}\]
can be defined as,
julia> @def_sde_system mutable struct MySDESystem{DR, DF, RO, ST, IP, OP} <: AbstractSDESystem
drift::DR = (dx, x, u, t) -> (dx .= -x)
diffusion::DF = (dx, x, u, t) -> (dx .= 1)
readout::RO = (x, u, t) -> x
state::ST = [1.]
input::IP = nothing
output::OP = Outport(1)
end
Defining DDE Dynamical System
The DDE dynamical system given by
\[ \begin{array}{l} \dot{x} = -x(t - \tau) \quad t \geq 0 \\ x(t) = 1. -\tau \leq t \leq 0 \\ \end{array}\]
can be defined as,
julia> _delay_feedback_system_cache = zeros(1)
1-element Array{Float64,1}:
0.0
julia> _delay_feedback_system_tau = 1.
1.0
julia> _delay_feedback_system_constlags = [1.]
1-element Array{Float64,1}:
1.0
julia> _delay_feedback_system_history(cache, u, t) = (cache .= 1.)
_delay_feedback_system_history (generic function with 1 method)
julia> function _delay_feedback_system_rhs(dx, x, h, u, t,
cache=_delay_feedback_system_cache, τ=_delay_feedback_system_tau)
h(cache, u, t - τ) # Update cache
dx[1] = cache[1] + x[1]
end
_delay_feedback_system_rhs (generic function with 3 methods)
julia> @def_dde_system mutable struct DelayFeedbackSystem{RH, HST, RO, IP, OP} <: AbstractDDESystem
constlags::Vector{Float64} = _delay_feedback_system_constlags
depslags::Nothing = nothing
righthandside::RH = _delay_feedback_system_rhs
history::HST = _delay_feedback_system_history
readout::RO = (x, u, t) -> x
state::Vector{Float64} = rand(1)
input::IP = nothing
output::OP = Outport(1)
end
Defining Sinks
Say we want a sink type that takes the data flowing through the connections of the model and prints it. This new sink type cane be defined as follows.
julia> @def_sink struct MySink{A} <: AbstractSink
action::A = actionfunc
end
julia> actionfunc(sink::MySink, t, u) = println(t, u)
actionfunc (generic function with 1 method)