Utilizing Graphs.jl

Goal

This tutorial illustrates how to integrate graphs that support the AbstractGraph interface from the Graphs.jl package into a Vahana simulation and the visualization support for Vahana graphs.

For this purpose, we implement the opinion dynamics model of Hegselmann and Krause (2002). An alternative implementation of the same model using the Agents.jl package can be found here.

Agent and Edge Types

using Vahana, Statistics

We have a finite number $n$ of agents, where the state of the agents are a real number $x_i(t)$ in the [0,1] interval which represents the opinion of that agent.

struct HKAgent
    opinion::Float64
end

This time we have only one network that determine the agents that will be considered when an agent updates its opinion.

struct Knows end

Beside these two types there is a confidence bound $\epsilon > 0$, opinions with a difference greater than $\epsilon$ are ignored by the agents in the transition function. All agents have the same confidence bound, so we introduce this bound as a parameter.

const hkmodel = ModelTypes() |>
    register_agenttype!(HKAgent) |>
    register_edgetype!(Knows) |>
    register_param!(:ϵ, 0.02) |> # the confidence bound
    create_model("Hegselmann-Krause");

Edgetype Knows is a struct without any field, so you can increase the
performance by setting the :Stateless hint. You can also
calling detect_stateless() before calling register_edgetype!,
then the :Stateless hint will be set automatically for structs without
a field.

Adding a Graph

Vahana allows adding SimpleGraphs and SimpleDiGraphs from the Graphs.jl package via the add_graph! function. So it is possible to use e.g. SNAPDatasets to run the opinion model on real datasets, or the SimpleGraphs module from Graphs.jl to create synthetic graphs.

We demonstrate here both use cases and are creating a separate simulation for each one.

const cgsim = create_simulation(hkmodel);
const snapsim = create_simulation(hkmodel)

Model Name: Hegselmann-Krause
Simulation Name: Hegselmann-Krause
Agent(s):
	 Type HKAgent with 0 agent(s)
Edge(s):
	 Type Knows with 0 edge(s) for 0 agent(s)
Parameter(s):
	 :ϵ : 0.02
Still in initialization process!.

SimpleGraphs

First we will show how we can add a synthetic graph. For this we need to import the SimpleGraphs module. Since there are many functions in the Graphs.jl package with the same name as in Vahana (e.g. add_edge!), it is advisable to import only the needed parts of Graphs.jl instead of loading the whole package via using Graphs.

import Graphs.SimpleGraphs

We want to add a complete graph, where each agent is connected with all the other agents, like in the Agents.jl implementation. We can create such a graph via SimpleGraphs.complete_graph.

g = SimpleGraphs.complete_graph(50)
{50, 1225} undirected simple Int64 graph

Vahana needs the information how to convert the nodes and edges of the SimpleGraphs object to the Vahana structure. This is done by the constructor functions in the third and forth arguments of add_graph!. We do not need the Graph.vertex and Graph.edge arguments of this constructor functions, but for other use cases e.g. for bipartite graphs, it would be possible to create agents of different types depending on this information.

const agentids = add_graph!(cgsim,
                            g,
                            _ -> HKAgent(rand()),
                            _ -> Knows());

Each agent also adds its own opinion to the calculation. We can use the ids returned by the add_graph! functions for this.

foreach(id -> add_edge!(cgsim, id, id, Knows()), agentids)

finish_init!(cgsim)

SNAPDataset.jl

The SNAPDataset.jl package delivers Graphs.jl formatted datasets from the Stanford Large Network Dataset Collection.

using SNAPDatasets

With this package we can use the loadsnap function to create the graph that is then added to the Vahana graph, e.g. in our example the facebook dataset.

const snapids = add_graph!(snapsim,
                           loadsnap(:facebook_combined),
                           _ -> HKAgent(rand()),
                           _ -> Knows());

Again each agent adds its own opinion to the calculation.

foreach(id -> add_edge!(snapsim, id, id, Knows()), snapids)

finish_init!(snapsim)

The Transition Function

Opinions are updated synchronously according to

\[\begin{aligned} x_i(t+1) &= \frac{1}{| \mathcal{N}_i(t) |} \sum_{j \in \mathcal{N}_i(t)} x_j(t)\\ \textrm{where } \quad \mathcal{N}_i(t) &= \{ j : \| x_j(t) - x_i(t) \| \leq \epsilon \} \end{aligned}\]

So we first filter all agents from the neighbors with an opinion outside of the confidence bound, and then calculate the mean of the opinions of the remaining agents.

function step(agent, id, sim)
    ϵ = param(sim, :ϵ)

    opinions = map(a -> a.opinion, neighborstates(sim, id, Knows, HKAgent))

    accepted = filter(opinions) do opinion
        abs(opinion - agent.opinion) < ϵ
    end

    HKAgent(mean(accepted))
end;

We can now apply the transition function to the complete graph simulation

apply!(cgsim, step, HKAgent, [ HKAgent, Knows ], HKAgent)

Model Name: Hegselmann-Krause
Simulation Name: Hegselmann-Krause
Agent(s):
	 Type HKAgent with 50 agent(s)
Edge(s):
	 Type Knows with 2500 edge(s) for 50 agent(s)
Parameter(s):
	 :ϵ : 0.02

Or to our facebook dataset

apply!(snapsim, step, HKAgent, [ HKAgent, Knows ], HKAgent)

Model Name: Hegselmann-Krause
Simulation Name: Hegselmann-Krause
Agent(s):
	 Type HKAgent with 4039 agent(s)
Edge(s):
	 Type Knows with 180507 edge(s) for 4039 agent(s)
Parameter(s):
	 :ϵ : 0.02

Creating Plots

Finally, we show the visualization possibilities for graphs, and import the necessary packages for this and create a colormap for the nodes.

import CairoMakie, GraphMakie, NetworkLayout, Colors, Graphs, Makie

Since the full graph is very cluttered and the Facebook dataset is too large, we construct a Clique graph using Graphs.jl.

const cysim = create_simulation(hkmodel) |> set_param!(:ϵ, 0.25);

const cyids = add_graph!(cysim,
                         SimpleGraphs.clique_graph(7, 8),
                         _ -> HKAgent(rand()),
                         _ -> Knows());


foreach(id -> add_edge!(cysim, id, id, Knows()), cyids)

finish_init!(cysim);

Vahana implements an interactive plot function based on GraphMakie, where agents and edges are given different colors per type by default, and the state of each agent/edge is displayed via mouse hover actions.

vp = create_graphplot(cysim)

figure(vp)
Example block output

To modify the created plot, the Makie figure, axis and plot, can be accessed via the methods figure, axis and plot. This allows us to modify the graph layout and to remove the decorations.

Vahana.plot(vp).layout = NetworkLayout.Stress()

Makie.hidedecorations!(axis(vp))

figure(vp)
Example block output

To visualize the agents' opinions, we can leverage Vahana's integration with Makie's plotting capabilities. Instead of directly modifying the node_color property of the Makie plot, we can define custom visualization functions. These functions, which can have methods for different agent and edge types, are used by create_graphplot to determine various properties of nodes and edges in the plot.

This approach not only allows for more dynamic and type-specific visualizations but also supports interactive plots when using GLMakie as the backend. We'll define a function called modify_vis that specifies how different elements should be displayed. This function will be passed to create_graphplot via the update_fn keyword argument.

colors = Colors.range(Colors.colorant"red", stop=Colors.colorant"green", length=100)


modify_vis(state::HKAgent, _ ,_) = Dict(:node_color => colors[state.opinion * 100 |> ceil |> Int],
                                       :node_size => 15)

modify_vis(_::Knows, _, _, _) = Dict(:edge_color => :lightgrey,
                                     :edge_width => 0.5);

function plot_opinion(sim)
    vp = create_graphplot(cysim,
                          update_fn = modify_vis)
    Vahana.plot(vp).layout = NetworkLayout.Stress()
    Makie.hidedecorations!(axis(vp))
    Makie.Colorbar(figure(vp)[:, 2]; colormap = colors)
    figure(vp)
end;

And now we can plot the initial state

plot_opinion(cysim)
Example block output

And then the state after 500 iterations

for _ in 1:500
    apply!(cysim, step, [ HKAgent ], [ HKAgent, Knows ], [ HKAgent ])
end

plot_opinion(cysim)
Example block output

Finish the Simulation

As always, it is important to call finish_simulation at the end of the simulation to avoid memory leaks.

finish_simulation!(cysim);

This page was generated using Literate.jl.