Network Diffusion

This introductory example explains the use of the basic types and constructors in NetworkDynamics.jl by modeling a simple diffusion on an undirected network.

This example can be dowloaded as a normal Julia script here.

Theoretical background

Diffusion processes are relevant for phenomena as diverse as heat conduction, electrical currents, and random walks. Generally speaking they describe the tendency of systems to evolve towards a state of equally distributed heat, charge or concentration. In such system the local temperature (or concentration) changes according to its difference with its neighborhood, i.e. the temperature gradient.

Let $g$ be a graph with $N$ nodes and adjacency matrix $A$. Let $v = (v_1, \dots, v_n)$ be a vector of (abstract) temperatures or concentrations at each node $i = 1, \dots, N$. Then the rate of change of state $v_i$ is described by its difference with its neighbors and we obtain the following ordinary differential equation

\[\dot v_i = \sum_{j=1}^N A_{ji} (v_j - v_i).\]

The sum on the right hand side plays the role of a (discrete) gradient. If the temperature at node $i$ is higher than at its neighboring node $j$ it will decrease along that edge.

Modeling diffusion in NetworkDynamics.jl

We begin by loading the necessary packages.

using Graphs
using NetworkDynamics
using OrdinaryDiffEqTsit5
using StableRNGs
using Plots

From the above considerations we see that in this model the nodes do not have any internal dynamics - if a node was disconnected from the rest of the network its state would never change, since then $A_{ji} = 0 \; \forall j$ and hence $\dot v_i = 0$. This means that the evolution of a node depends only on the interaction with its neighbors. In NetworkDynamics.jl, interactions with neighbors are described by equations for the edges.

In order to bring this equation into the form required by NetworkDynamics.jl we need split the dynamics into edge and vertex parts and bring them into the correct input-output formulation. The vertices have one internal state $v$ which is also the output. The input is the sum over all flows of connected edges. This directly correspons to the component model definition outlined in Mathematical Model:

\[\begin{aligned} \dot x^\mathrm{v} &= f^{\mathrm v}(u^{\mathrm v}, \sum_k^{\text{incident}} y^{\mathrm e}_k, p^{\mathrm v}, t) &&= \sum_k^\mathrm{incident} y^\mathrm{e}_k \\ y^\mathrm{v} &= g^{\mathrm v}(u^{\mathrm v}, \sum_k^{\text{incident}} y^{\mathrm e}_k, p^{\mathrm v}, t) &&= x^\mathrm{v} \end{aligned}\]

The edge dynamics on the other hand do not have any internal states. Thus we only define the output as the difference between the source and destination vertex:

\[\begin{aligned} y^{\mathrm e}_{\mathrm{dst}} &= g_\mathrm{dst}^{\mathrm e}(u^{\mathrm e}, y^{\mathrm v}_{\mathrm{src}}, y^{\mathrm v}_{\mathrm{dst}}, p^{\mathrm e}, t) &&= y^{\mathrm v}_{\mathrm{src}} - y^{\mathrm v}_{\mathrm{dst}}\\ y^{\mathrm e}_{\mathrm{src}} &= g_\mathrm{src}^{\mathrm e}(u^{\mathrm e}, y^{\mathrm v}_{\mathrm{src}}, y^{\mathrm v}_{\mathrm{dst}}, p^{\mathrm e}, t) &&= y^{\mathrm v}_{\mathrm{dst}} - y^{\mathrm v}_{\mathrm{src}} \end{aligned}\]

Definition of EdgeModel

function diffusionedge_g!(e_dst, v_src, v_dst, p, t)
    # e_dst, v_src, v_dst are arrays, hence we use the broadcasting operator
    e_dst .= v_src .- v_dst
    nothing
end

The function diffusionedge_g! takes as inputs the current state of the edge e, its source vertex v_src, its destination vertex v_dst, a vector of parameters p and the time t. In order to comply with the syntax of NetworkDynamics.jl we always have to define functions for edges with exactly these arguments, even though we do not need p and t for the diffusion example.

diffusionedge_g! is called a mutating function, since it modifies (or mutates) one of its inputs, namely the edge state e. As a convention in Julia names of mutating functions end with an !. The use of mutating functions reduces allocations and thereby speeds up computations. After the function call the edge's output value e equals the difference between its source and its destination vertex (i.e. the discrete gradient along that edge).

Notably, this function only models $g_\mathrm{dst}$. However we can wrap this single-sided output function in an AntiSymmetric output wrapper to construct the EdgeModel:

nd_diffusion_edge = EdgeModel(; g=AntiSymmetric(diffusionedge_g!), outsym=[:flow])
EdgeModel :StaticEdgeM PureFeedForward()
 ├─   0 states:  []  
 └─ 1/1 outputs: src=[₋flow] dst=[flow]

Definition of VertexModel

For undirected graphs, the edgefunction! specifies the coupling from a source- to a destination vertex. The contributions of the connected edges to a single vertex are "aggregated". Default aggregation is the summation of all incident edge states. The aggregated edge state is made available via the esum argument of the vertex function.

function diffusionvertex_f!(dv, v, esum, p, t)
    # dv, v and esum are arrays, hence we use the broadcasting operator .
    dv .= esum
    nothing
end

Just like above the input arguments v, esum, p, t are mandatory for the syntax of vertex functions. The additional input dv corresponding to the derivative of the vertex' state is mandatory for vertices described by ordinary differential equations.

The output function g is just taking part of the internal states. For that we can use the StateMask helper function g = StateMaks(1:1)

nd_diffusion_vertex = VertexModel(; f=diffusionvertex_f!, g=StateMask(1:1), dim=1)
VertexModel :VertexM PureStateMap()
 ├─ 1 state:  [s]
 └─ 1 output: [s]

Constructing the network

With the components defined, we can define the topology and assemble the network dynamics.

N = 20 # number of nodes
k = 4  # average degree
g = barabasi_albert(N, k) # a little more exciting than a bare random graph

The Barabási–Albert model generates a scale-free random graph.

nd = Network(g, nd_diffusion_vertex, nd_diffusion_edge)
Network with 20 states and 0 parameters
 ├─ 20 vertices (1 unique type)
 └─ 64 edges (1 unique type)
Edge-Aggregation using SequentialAggregator(+)

The constructor Network combines the component model with the topological information contained in the graph g and returns an Network compatible with the solvers of DifferentialEquations.jl.

rng = StableRNG(1)
x0 = randn(rng, N) # random initial conditions
ode_prob = ODEProblem(nd, x0, (0.0, 2.0))
sol = solve(ode_prob, Tsit5());

We are solving the diffusion problem on the time interval $[0, 2]$ with the Tsit5() algorithm, which is recommended by the authors of DifferentialEquations.jl for most non-stiff problems.

plot(sol; idxs=vidxs(nd, :, :), fmt=:png)
Example block output

The plotting is straightforward. The idxs keyword allows us to pass a list of indices. Indices can be also "symbolic" indices which specify components and their symbols directly. For example idxs = VIndex(1, :v) acesses state :v of vertex 1. See Symbolic Indexing for more details.

In oder to collect multiple indices we can use the helper function vidxs and eidxs, which help to collect all symbolic indices matching a certain criteria.

Two Dimensional Extension

To illustrate a very simple multi-dimensional case, in the following we simulate two independent diffusions on an identical graph. The first uses the symbol x and is started with initial conditions drawn from the standard normal distribution $N(0,1)$, the second uses the symbol ϕ with squared standard normal inital conditions.

The symbols have to be passed with the keyword sym to VertexModel.

N = 10 # number of nodes
k = 4  # average degree
g = barabasi_albert(N, k) # a little more exciting than a bare random graph

# We will have two independent diffusions on the network, hence dim = 2
nd_diffusion_vertex_2 = VertexModel(; f=diffusionvertex_f!, g=1:2, dim=2, sym=[:x, :ϕ])
nd_diffusion_edge_2 = EdgeModel(; g=AntiSymmetric(diffusionedge_g!), outsym=[:flow_x, :flow_ϕ])
nd_2 = Network(g, nd_diffusion_vertex_2, nd_diffusion_edge_2)

x0_2 = vec(transpose([randn(rng, N) .^ 2 randn(rng, N)])) # x ~ N(0,1)^2; ϕ ~ N(0,1)
ode_prob_2 = ODEProblem(nd_2, x0_2, (0.0, 3.0))
sol_2 = solve(ode_prob_2, Tsit5());

Try plotting the variables ϕ_i yourself. [To write ϕ type \phi and press TAB]

plot(sol_2; idxs=vidxs(nd_2, :, :x), fmt=:png)
Example block output

Using the eidxs helper function we can also plot the flow variables

plot(sol_2; idxs=eidxs(nd_2, :, :flow_x), fmt=:png)
Example block output

Appendix: The network Laplacian $L$

The diffusion equation on a network can be rewritten as

\[\dot v_i = \sum_{j=1}^N A_{ji} v_j - d_i v_i = e_i^T A v - d_i v_i\]

where $d_i$ is the degree of node $i$ and $e_i^T$ is the $i$-th standard basis vector. Introducing the diagonal matrix $D$ that has the degree of node $i$ in its $i$-th row and the Laplacian matrix $L = D - A$ we arrive at

\[\dot v = e_i^T(A - D) v\]

and finally

\[\dot v = - L v\]

This is a linear system of ODEs and its solution is a matrix exponential. To study the asymptotic behaviour of the system it suffices to analyze the eigenspectrum of $L$. For this reason $L$ is an important construction in network science.


This page was generated using Literate.jl.