Defining New Component Types

Causal provides a library that includes some well-known components that are ready to be used. For example,

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.

Warning

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.

MacroComponent TypeSupertypeMandatory Field Names
@def_sourceSourceAbstractSourcereadout, output
@def_static_systemStaticSystemAbstractStaticSystemreadout, output, input
@def_discrete_systemDiscrete Dynamic SystemAbstractDiscreteSystemrighthandside, readout, state, input, output
@def_ode_systemODE Dynamic SystemAbstractODESystemrighthandside, readout, state, input, output
@def_dae_systemDAE Dynamic SystemAbstractDAESystemrighthandside, readout, state, stateder, diffvars, input, output
@def_rode_systemRODE Dynamic SystemAbstractRODESystemrighthandside, readout, state, input, output
@def_sde_systemSDE Dynamic SystemAbstractSDESystemdrift, diffusion, readout, state, input, output
@def_dde_systemDDE Dynamic SystemAbstractDDESystemconstlags, depslags, righthandside, history, readout, state, input, output
@def_sinkSinkAbstractSinkaction

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)