Network Diffusion

Getting started with NetworkDynamics.jl via tvbo

Replicates the NetworkDynamics.jl Getting Started tutorial — diffusion on an undirected network with 20 nodes. A single scalar state per node, antisymmetric diffusive coupling, and convergence to equilibrium.

YAML Specification

# =============================================================================
# Network Diffusion - Getting Started with NetworkDynamics.jl
# =============================================================================
# Source: https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/getting_started_with_network_dynamics/
#
# Diffusion on an undirected network. Each node state evolves as the sum of
# incoming edge flows.
# Vertex has no intrinsic dynamics; edge is antisymmetric: e = v_src - v_dst.
# =============================================================================

id: 100
label: "Network Diffusion"
description: >
    Diffusion on an undirected network.  Each node state evolves as the sum
    of incoming edge flows.  The edge computes the antisymmetric difference
    between source and destination states: e = v_src - v_dst.
    Recreates the NetworkDynamics.jl Getting Started tutorial.
references:
    - "https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/getting_started_with_network_dynamics/"

# -- Local Dynamics ------------------------------------------------------------
dynamics:
    name: Diffusion
    label: "Diffusion vertex"
    description: >
        Node with no internal dynamics.  The rate of change of the single
        state variable v equals the aggregated incoming edge flow (esum).
        Corresponds to the discrete Laplacian: dx_i = sum_j A_ji (x_j - x_i).
    system_type: continuous
    autonomous: true
    parameters: {}
    coupling_inputs:
        c_flow:
            description: "Aggregated edge flow into this vertex (sum of incident edge outputs)"
    state_variables:
        v:
            label: "Diffusion state"
            description: "Abstract quantity (e.g. temperature, concentration) diffusing on the network"
            equation:
                rhs: "c_flow"
            initial_value: 0.0
            distribution:
                name: Gaussian
                seed: 1
                domain: { lo: -3.0, hi: 3.0 }
            variable_of_interest: true

# -- Network -------------------------------------------------------------------
network:
    label: "20-node Barabási-Albert network"
    description: >
        Undirected Barabási-Albert scale-free random graph with 20 nodes
        and average degree 4.  Matches the original tutorial.
    number_of_nodes: 20
    graph_generator:
        name: barabasi_albert
        type: BarabasiAlbert
        parameters:
            k: { name: k, value: 4 }
    coupling:
        DiffusiveCoupling:
            name: DiffusiveCoupling
            label: "Diffusive Coupling"
            description: >
                Antisymmetric edge function: output is the difference between
                source and destination vertex states.
                g_dst(v_src, v_dst) = v_src - v_dst;
                g_src(v_src, v_dst) = v_dst - v_src (by AntiSymmetric wrapper).
            delayed: false
            pre_expression:
                rhs: "x_j - x_i"
                description: "Discrete gradient: source state minus destination state"

# -- Integration ---------------------------------------------------------------
integration:
    description: "Explicit Runge-Kutta (Tsit5) with dt=0.01 over t in [0, 2]"
    method: Tsit5
    step_size: 0.01
    duration: 2.0
    time_scale: "a.u."

Model Report

* Owlready2 * Warning: ignoring cyclic subclass of/subproperty of, involving:
  http://uri.interlex.org/tgbugs/uris/readable/atlas/Space

Network Diffusion

Diffusion on an undirected network. Each node state evolves as the sum of incoming edge flows. The edge computes the antisymmetric difference between source and destination states: e = v_src - v_dst. Recreates the NetworkDynamics.jl Getting Started tutorial.

Local Dynamics: Diffusion vertex

Node with no internal dynamics. The rate of change of the single state variable v equals the aggregated incoming edge flow (esum). Corresponds to the discrete Laplacian: dx_i = sum_j A_ji (x_j - x_i).

system: continuous; autonomous: True; modes: 1; state variables: 1; parameters: 0.

State Equations

\[\dot{v} = c_{flow}\]

State Variables

Variable Initial Value Equation Domain / Sampling Flags Description
\(v\) 0 differential (order 1) Gaussian [-3, 3] axis=space seed=1 recorded Abstract quantity (e.g. temperature, concentration) diffusing on the network

Dimensional Check

Equation Dimensional check Detail
\(\dot{v}\) underdetermined v has no declared unit

Coupling Inputs

Input Source Dimension Keys Description
c_flow 1 Aggregated edge flow into this vertex (sum of incident edge outputs)

Brain Network: 20-node Barabási-Albert network

Undirected Barabási-Albert scale-free random graph with 20 nodes and average degree 4. Matches the original tutorial.

Setting Value
Regions 20
Conduction velocity 3.0 mm_per_ms
Distance unit mm
Nodes 20 explicit nodes
Graph generator BarabasiAlbert
Weights shape=20x20, dtype=float64

Coupling: Diffusive Coupling

Antisymmetric edge function: output is the difference between source and destination vertex states. g_dst(v_src, v_dst) = v_src - v_dst; g_src(v_src, v_dst) = v_dst - v_src (by AntiSymmetric wrapper).

Property Value
Symmetry directed

Pre-synaptic: \(c_{\text{pre}} = x_{j} - x_{i}\)

Numerical Integration

Explicit Runge-Kutta (Tsit5) with dt=0.01 over t in [0, 2]

Setting Value
Method Tsit5
Time step \(\Delta t = 0.01\) ms
Duration 2.0 ms
Absolute tolerance 1e-10
Relative tolerance 1e-10
Stages 1
Delayed state history True

References

  • https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/getting_started_with_network_dynamics/

Generated Julia Code

from tvbo import SimulationExperiment
exp = SimulationExperiment.from_file("yaml/diffusion.yaml")
Code(exp.render_code("networkdynamics"), language='julia')
using Graphs
using NetworkDynamics
using OrdinaryDiffEqTsit5


function Diffusion_f!(dx, x, esum, p, t)
    v = x[1]

    c_flow = esum[1]
    dx[1] = c_flow
    nothing
end

vertex_Diffusion = VertexModel(;
    f = Diffusion_f!,
    g = StateMask(1:1),
    sym = [:v],
    name = :Diffusion,
)


function DiffusiveCoupling_edge_g!(e_dst, v_src, v_dst, p, t)
    e_dst[1] = v_src[1] .- v_dst[1]
    nothing
end

edge_DiffusiveCoupling = EdgeModel(;
    g = AntiSymmetric(DiffusiveCoupling_edge_g!),
    outsym = [:coupling],
    name = :DiffusiveCoupling,
)


g = barabasi_albert(20, 4)


nw = Network(g, vertex_Diffusion, edge_DiffusiveCoupling)


s = NWState(nw)
using Random
rng = MersenneTwister(1)

for node in 1:nv(g)
    s.v[node, :v] = (-3.0 + 3.0) / 2 .+ (3.0 - -3.0) / 6 .* randn(rng)
end


tspan = (0.0, 2.0)
prob = ODEProblem(nw, uflat(s), tspan, pflat(s))
sol = solve(prob, Tsit5(); saveat=0.01)

adj_matrix = Float64.(adjacency_matrix(g))

using Random: MersenneTwister
function spring_layout(g; seed=42, iterations=50, k=1.0)
    rng = MersenneTwister(seed)
    n = nv(g)
    pos = randn(rng, n, 2)
    for _ in 1:iterations
        disp = zeros(n, 2)
        for i in 1:n, j in (i+1):n
            d = pos[i, :] - pos[j, :]
            dist = max(norm(d), 0.01)
            rep = k^2 / dist
            disp[i, :] .+= d / dist * rep
            disp[j, :] .-= d / dist * rep
        end
        for e in edges(g)
            i, j = src(e), dst(e)
            d = pos[j, :] - pos[i, :]
            dist = max(norm(d), 0.01)
            att = dist^2 / k
            disp[i, :] .+= d / dist * att
            disp[j, :] .-= d / dist * att
        end
        for i in 1:n
            dl = max(norm(disp[i, :]), 0.01)
            pos[i, :] .+= disp[i, :] / dl * min(dl, 0.1)
        end
    end
    return pos
end
using LinearAlgebra: norm
node_positions = spring_layout(g)

using Plots
plot(sol; ylabel="state", xlabel="time", title="Diffusion on 20-node network")

Run & Plot

ts = exp.run(format="networkdynamics")
ts.plot()
Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython

Results

Shape: (201, 1, 20)
Time range: 0.0 – 2.0
Final state std: 0.002159
(Should converge to ~0 as all nodes reach equilibrium)

Animate

from pathlib import Path

ani = ts.animate("v", format="dots")
Path("_output").mkdir(exist_ok=True)
ani.save("_output/diffusion.gif", writer="pillow", fps=20)

Node values on a random graph relaxing toward a common value as diffusion spreads along the edges.