I have to admit I somewhat nerd-sniped myself here thinking about this
I think that it might be helpful to switch from a vertex centered view (that is, agent) to a viewpoint of the model focused on the edges. It seems to me that apart from the “idle” indicator associated to the vertices, most of the “dynamics” of the model are focused on the edges, in particular the main stochastic element which is the random waiting time.
From my perspective, it seems like the crucial thing to get right when considering the event scheduling is calculating the legal event sets. Meaning given a network and the given interaction rules (an agent can only be involved in one interaction at a time), one needs to compute all the valid combinations of edges which can be scheduled at a given point in time, given the network state.
Using the network setup in your previous post (with the Gantt chart), I had a go at calculating all the legal firing sets, respecting the conflicts (i.e. if the B-D interaction starts, the D-A, B-A, etc interactions cannot start). I used Catlab.jl for the edge-list style graph data structure, but there is nothing categorical going on in this code. Also, forgive the messy and inefficient algorithm but I had to prevent myself spending more time on this.
using Catlab
g = @acset LabeledGraph{Symbol} begin
V=6
label=[:A,:B,:C,:D,:E,:F]
end
add_edges!(
g,
vcat(incident(g, [:A,:A,:B,:B,:D,:E,:C], :label)...),
vcat(incident(g, [:B,:D,:D,:E,:C,:F,:F], :label)...)
)
to_graphviz(g, node_labels=:label, edge_labels=true)
# 1. for each node, grab all edges adjacent
# these are the "conflict set" for each node, only 1 edge in the set can fire at a given time
V_conflict = Dict([
let
g[v,:label] => Set(union(incident(g, v, :src), incident(g, v, :tgt)))
end
for v in vertices(g)
])
# 2. for each edge, union all the node conflict sets adjacent to its endpoints, and subtract itself
# this is the "conflict set" for each edge, if it fires, none of those may fire
E_conflict = Dict([
let
v1 = g[e,:src]
v2 = g[e,:tgt]
e => setdiff(union(V_conflict[g[v1,:label]], V_conflict[g[v2,:label]]), [e])
end
for e in edges(g)
])
# 3. compute legal firing sets
legal_firings = Set{Set{Int64}}()
for (e, e_conflict) in E_conflict
# E is remaining set of edges which may fire
E = Set(edges(g))
setdiff!(E, e)
setdiff!(E, e_conflict)
# the legal firing set
f = Set([e])
# iterate over remaining edges (one path in the search tree)
if !isempty(E)
for e′ in E
f′ = deepcopy(f)
E′ = deepcopy(E)
setdiff!(E′, e′)
setdiff!(E′, E_conflict[e′])
push!(f′, e′)
while !isempty(E′)
e′′ = pop!(E′)
setdiff!(E′, e′′)
setdiff!(E′, E_conflict[e′′])
push!(f′, e′′)
end
push!(legal_firings, f′)
end
else
push!(legal_firings, f)
end
end
legal_firings
Given a state at “time 0” where no agents are busy, this should recover all of the legal events in legal_firings
. Each element in the set legal_firings
is a set of edges which may simultaneously begin their respective interactions, and only one element of legal_firings
may be scheduled before updating state and recomputing that set. I used the term “firing” because of the discrete event nature of the simulation, even though it is possible for multiple atomic events to occur at a single point in time (i.e. multiple interactions begin).
Your specific parameters regarding priority, etc, could be used to weight each element and sample over them. I assumed that any enabled edge’s interaction will occur given the opportunity, which is why you see Set([4, 7, 2])
as a legal event but not 4 or 7 or 2 individually, for example.