Models and Graphs

This tutorial illustrates the relation 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.

Construction of Models

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

model

and with the following signal-flow graph

model

Let's start with an empty Model.

julia> using Jusdl # hide

julia> model = Model()
Model(numnodes:0, numedges:0, timesettings=(0.0, 0.01, 1.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((+,-)), 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/a32ea72b-9faa-4fa5-8bdc-46893b358e39.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(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 Array{Any,1}:
 Branch(nodepair:1 => 2, indexpair:1 => 1, links: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(state:open, eltype:Float64, isreadable:false, iswritable:false))

julia> br.nodepair
1 => 2

julia> br.indexpair
1 => 1

julia> br.links
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(state:open, eltype:Float64, isreadable:false, iswritable:false))

julia> addbranch(model, :gain => :adder, 1 => 2)
Branch(nodepair:3 => 2, indexpair:1 => 2, links: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(state:open, eltype:Float64, isreadable:false, iswritable:false))

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 Array{Array{Int64,1},1}:
 [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, 2]  =  1
  [3, 2]  =  1
  [2, 3]  =  1
  [3, 4]  =  1

or inneighbors or outneighbors of a node can be obtained.

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

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