# Model Construction

This tutorial illustrates model construction and the relation between models and graphs. A model consists of components and connections. These components and connections can be associated with a signal-flow graph signifying the topology of the model. In the realm of graph theory, components and connections of a model are associated with nodes and branches of the signal-flow graph. As the model is modified by adding or deleting components or connections, the signal-flow graph of the model is modified accordingly to keep track of topological modifications. By associating a signal-flow graph to a model, any graph-theoretical analysis can be performed. An example to such an analysis is the determination and braking of algebraic loops.

In Causal, a model can be constructed either by describing it in one-shot or by gradually modifying it by adding new nodes and branches. To show the relation between models and graphs, we start with the latter.

## Modifying Models

In this tutorial, we construct the model with the following block diagram

and with the following signal-flow graph

Let's start with an empty `Model`

.

```
julia> using Causal # hide
julia> model = Model()
Model(numnodes:0, numedges:0)
```

We constructed an empty model, i.e., the model has no components and connections. To modify the model, we need to add components and connections to the model. As the model is grown by adding components and connections, the components and connections are added into the model as nodes and branches (see `Node`

, `Branch`

). Let's add our first component, a `SinewaveGenerator`

to the `model`

.

```
julia> addnode!(model, SinewaveGenerator(), label=:gen)
Node(component:SinewaveGenerator(amp:1.0, freq:1.0, phase:0.0, offset:0.0, delay:0.0), idx:1, label:gen)
```

To add components to the `model`

, we use `addnode!`

function. As seen, our node consists of a component, an index, and a label.

```
julia> node1 = model.nodes[1]
Node(component:SinewaveGenerator(amp:1.0, freq:1.0, phase:0.0, offset:0.0, delay:0.0), idx:1, label:gen)
julia> node1.component
SinewaveGenerator(amp:1.0, freq:1.0, phase:0.0, offset:0.0, delay:0.0)
julia> node1.idx
1
julia> node1.label
:gen
```

Let us add another component, a `Adder`

, to the model,

```
julia> addnode!(model, Adder(signs=(+,-)), label=:adder)
Node(component:Adder(signs:(+, -), input:Inport(numpins:2, eltype:Inpin{Float64}), output:Outport(numpins:1, eltype:Outpin{Float64})), idx:2, label:adder)
```

and investigate our new node.

```
julia> node2 = model.nodes[2]
Node(component:Adder(signs:(+, -), input:Inport(numpins:2, eltype:Inpin{Float64}), output:Outport(numpins:1, eltype:Outpin{Float64})), idx:2, label:adder)
julia> node2.component
Adder(signs:(+, -), input:Inport(numpins:2, eltype:Inpin{Float64}), output:Outport(numpins:1, eltype:Outpin{Float64}))
julia> node2.idx
2
julia> node2.label
:adder
```

Note that as new nodes are added to the `model`

, they are given an index `idx`

and a label `label`

. The label is not mandatory, if not specified explicitly, `nothing`

is assigned as label. The reason to add components as nodes is to access them through their node index `idx`

or `labels`

. For instance, we can access our first node by using its node index `idx`

or node label `label`

.

```
julia> getnode(model, :gen) # Access by label
Node(component:SinewaveGenerator(amp:1.0, freq:1.0, phase:0.0, offset:0.0, delay:0.0), idx:1, label:gen)
julia> getnode(model, 1) # Access by index
Node(component:SinewaveGenerator(amp:1.0, freq:1.0, phase:0.0, offset:0.0, delay:0.0), idx:1, label:gen)
```

At this point, we have two nodes in our model. Let's add two more nodes, a `Gain`

and a `Writer`

```
julia> addnode!(model, Gain(), label=:gain)
Node(component:Gain(gain:1.0, input:Inport(numpins:1, eltype:Inpin{Float64}), output:Outport(numpins:1, eltype:Outpin{Float64})), idx:3, label:gain)
julia> addnode!(model, Writer(), label=:writer)
Node(component:Writer(path:/tmp/9d7b46b1-773b-45b2-84f9-70e5d069337a.jld2, nin:1), idx:4, label:writer)
```

As the nodes are added to the `model`

, its graph is modified accordingly.

```
julia> model.graph
{4, 0} directed simple Int64 graph
```

`model`

has no connections. Let's add our first connection by connecting the first pin of the output port of the node 1 (which is labelled as `:gen`

) to the first input pin of input port of node 2 (which is labelled as `:adder`

).

```
julia> addbranch!(model, :gen => :adder, 1 => 1)
Branch(nodepair:1 => 2, indexpair:1 => 1, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
```

The node labelled with `:gen`

has an output port having one pin, and the node labelled with `:adder`

has an input port of two pins. In our first connection, we connected the first(and the only) pin of the output port of the node labelled with `:gen`

to the first pin of the input port of the node labelled with `:adder`

. The connections are added to model as branches,

```
julia> model.branches
1-element Vector{Any}:
Branch(nodepair:1 => 2, indexpair:1 => 1, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
```

A branch between any pair of nodes can be accessed through the indexes or labels of nodes.

```
julia> br = getbranch(model, :gen => :adder)
Branch(nodepair:1 => 2, indexpair:1 => 1, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
julia> br.nodepair
1 => 2
julia> br.indexpair
1 => 1
julia> br.links
1-element Vector{Link{Float64}}:
Link(state:open, eltype:Float64, isreadable:false, iswritable:false)
```

Note the branch `br`

has one link(see `Link`

). This is because we connected one pin to another pin. The branch that connects $n$ pins to each other has `n`

links. Let us complete the construction of the model by adding other connections.

```
julia> addbranch!(model, :adder => :gain, 1 => 1)
Branch(nodepair:2 => 3, indexpair:1 => 1, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
julia> addbranch!(model, :gain => :adder, 1 => 2)
Branch(nodepair:3 => 2, indexpair:1 => 2, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
julia> addbranch!(model, :gain => :writer, 1 => 1)
Branch(nodepair:3 => 4, indexpair:1 => 1, links:Link{Float64}[Link(state:open, eltype:Float64, isreadable:false, iswritable:false)])
```

## Describing Models

The second approach is to describe the whole model. In this approach the model is constructed in single-shot. The syntax here is

```
@defmodel modelname begin
@nodes begin
label1 = Component1(args...; kwargs...) # Node 1
label2 = Component2(args...; kwargs...) # Node 2
⋮ ⋮
labelN = ComponentN(args...; kwargs...) # Node N
end
@branches begin
src_label1[src_index1] = dst_label1[dst_index1] # Branch 1
src_label2[src_index2] = dst_label1[dst_index2] # Branch 2
⋮ ⋮
src_labelM[src_indexM] = dst_labelM[dst_indexM] # Branch M
end
end
```

Note that `modelname`

is the name of the model to be compiled. The nodes of the model is defined in `@nodes begin ... end`

block and the branches of the model is defined in `@branches begin ... end`

. The syntax `src_label1[src_index1] = dst_label1[dst_index1]`

means that there is a branch between the node labelled with `src_label1`

and the node labelled with `dst_label1`

.And, this branch connects the pins indexed by `src_index1`

of the output port of `src_label1`

to the pins indexed by `dst_index1`

of the input port of `dst_label1`

. The indexing of the pins here is just like any one dimensional array indexing. That is `src_index1`

( or `dst_index1`

) may be integer, vector of integers, vector of booleans, range, etc.

For example, the model given above can also be constructed as follows

```
julia> using Causal # hide
julia> @defmodel model begin
@nodes begin
gen = SinewaveGenerator()
adder = Adder(signs=(+,-))
gain = Gain()
writer = Writer()
end
@branches begin
gen[1] => adder[1]
adder[1] => gain[1]
gain[1] => adder[2]
gain[1] => writer[1]
end
end
```

This macro is expanded to construct the `model`

.

## Usage of Signal-Flow Graph

The signal-flow graph constructed alongside of the construction of the model can be used to perform any topological analysis. An example to such an analysis is the detection of algebraic loops. For instance, our model in this tutorial has an algebraic loop consisting of the nodes labelled with `:gen`

and `gain`

. This loop can be detected using the signal-flow graph of the node

```
julia> loops = getloops(model)
1-element Vector{Vector{Int64}}:
[2, 3]
```

We have one loop consisting the nodes with indexes 2 and 3.

For further analysis on model graph, we use `LightGraphs`

package.

```
julia> using LightGraphs
julia> graph = model.graph
{4, 4} directed simple Int64 graph
```

For example, the adjacency matrix of model graph can be obtained.

```
julia> adjacency_matrix(model.graph)
4×4 SparseArrays.SparseMatrixCSC{Int64, Int64} with 4 stored entries:
⋅ 1 ⋅ ⋅
⋅ ⋅ 1 ⋅
⋅ 1 ⋅ 1
⋅ ⋅ ⋅ ⋅
```

or inneighbors or outneighbors of a node can be obtained.

```
julia> inneighbors(model.graph, getnode(model, :adder).idx)
2-element Vector{Int64}:
1
3
julia> outneighbors(model.graph, getnode(model, :adder).idx)
1-element Vector{Int64}:
3
```