Interacting agents in discrete event simulations

Hello,

I would like to develop a simulation in which agents interact in pairs for a random period of time. While a pair is interacting, the agents in the pair are “busy”, meaning they are not available to interact with other agents. Each agent interacts with each agent it is connected to in the network only once. The order of execution should be random, subject to the previously described constraints. It is important to keep track of simulated time but I do not want to pause the cpu with sleep to emulate time. I am not entirely sure how to set up the simulation. Should I use continuous time models in Agents.jl with channels or other Julia functions, or should I leverage something like DiscreteEvents.jl?

Any advice or a MWE would be great. Here is an incomplete sketch of what I would like to do:

struct Agent 
    id::Int 
    connections::Vector{Int}
    is_busy::Bool 
    remaining_ids::Vector{Int} 
end

agents = [
    Agent(1, [2,3,4], false, [2,3,4]),
    Agent(2, [1,3,4], false, [1,3,4]),
    Agent(3, [2,1,4], false, [2,1,4]),
    Agent(4, [2,3,1], false, [2,3,1]),
]

function interact(agent1, agent2)
    agent1.busy = true
    agent2.busy = true 
    # interact for a variable amount of time, then update busy = false  
end

function find_available_agent(agent)
    # find an agent id in agent.available_ids available 
end

function remove_id(agent)
    # remove id from agent.available_ids after interacting 
end

Hi Christopher!
You’re right that when you choose between DiscreteEvents.jl and Agents.jl, you’re choosing between continuous-time and discrete-time. Writing the code for Agents.jl might be simpler because you just make that one loop. If you’re comparing this simulation with some kind of integration of a mathematical model, it could be closer with the continuous-time (but you can also use the discrete time for small time steps and usually get away with it).

I don’t have a minimal working example (MWE, I looked that up). For advice: be clear about states and transitions of those states. That’s kinda it. If you want to convert between continuous-time and discrete time, there are some really nice worked-through examples in papers by Linda Allen.

Hope All Is Well,
Drew

Thank you for your reply. I’m not quite sure I can use a loop with Agents.jl to achieve my goal. I’m sure I can use DiscreteEvents.jl, but I have yet to figure out the right approach.

I am attaching a gantt plot to illustrate a potential scenario. Here there are 6 agents, A,B,C,D,E,F, and G. Each agent interacts twice, each time with a different agent. An agent can only interact with an agent in its social network, and the interactions are scheduled so as to minimize wait time. The connections are as follows:
A: [B,D]
B: [A,D,E]
C: [D,F]
D: [A,C]
E: [B,F,]
F: [C,E]

In the example below, the following pairs interact first: (A,B), (C,D), and (E,F). A and B finish first, but have to wait for the a connected agent to become available. The next available agent for B is agent E. Agent A has to wait even for longer for agent D to become available. Agent F waits to interact with agent C.

Hi @Christopher_Fisher this sounds interesting! I’d like to help provide some suggestions and maybe an MWE but I need to know more about the simulation. In particular, how does your envisioned stochastic process handle selection of which pair begins after an interaction finishes. Let me illustrate my question by way of example (the agent names are unrelated to your Gantt chart).

Say A and E have a scheduled interaction that will finish first among all currently ongoing interactions. When their interaction finishes, they are both now “idle”. Say both A and E have remaining agents they have not interacted with in their respective remaining_ids pool. These 2 pools are disjoint except for, say, agent B, who is “idle” at this moment. What is the stochastic mechanism which determines who A and E will interact with at this point in time? Obviously they cannot both simultaneously interact with B, based on the problem description. Based on the feasible interactions at this point in time, there should be sets of interactions, each of which is mutually exclusive (i.e. either A or E interacts with B), and all interactions within each set should be feasible (i.e. within some set, we may have A interacting with B, and E interacting with some other idle neighbor in their pool, say G).

1 Like

Thank you for your willingness to help me with this problem! I was contemplating solutions with channels and DiscretEvents.jl, but ran into several dead ends, most likely due to my lack of familiarity with complex event scheduling.

This is a good question and forces me to clarify what I mean by minimize waiting time. In your example, it would be sufficient to select an agent at random from the set {A,E}. Once an agent is selected, let’s say A, the next step would be to select an agent from A’s remaining_ids pool that is currently idle. If there are multiple idle agents in the pool, this would be resolved by random selection. I am not familiar with the terminology, but what I described might qualify as a “first come, first serve” schedule, subject to the contraints above and random selection to resolve ties. So this means that the system does not need to globally minimize wait time. I suspect this makes the set up easier to complete.

To give you a bit of background into my model, the agents I am creating have a memory system in which memories decay gradually over time. This is why I would like to track time and reduce wait time between interactions. I think this would be more natural than what can be achieved with the discrete time step default in Agents.jl.

Thanks again for your help, and let me know if I can help or provide more clarification.

I have to admit I somewhat nerd-sniped myself here thinking about this :sweat_smile:

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.

1 Like

@slwu89, thank you for explaining your example with graphs. One thing I really like is how the graph representation makes it easy to reason about legal sets of interactions. The plots are very helpful.

If I understand correctly, a new set for legal_firings must be recalculated after each interaction. For example, if the set of vertices {4,7,2} is randomly selected, 4, 7,2 cannot be selected according to the constraints above. I tried to filter out sets containing 4,7, or 2 from legal_fires, but this approach eventually led to a situation in which some interactions would not occur. Is this true, or am I misunderstanding something? See this example:

Example
legal_firings

Set{Set{Int64}} with 7 elements:
  Set([7, 3])
  Set([6, 3])
  Set([4, 7, 2])
  Set([5, 6, 1])
  Set([6, 2])
  Set([5, 4])
  Set([7, 1])

# select a random set to start the interaction 
interacting_set = rand(legal_firings)

Set{Int64} with 3 elements:
  4
  7
  2
# filter out sets containing 4,7 or 2
filter!(x -> isempty(intersect(interacting_set, x)), legal_firings)

Set{Set{Int64}} with 2 elements:
  Set([6, 3])
  Set([5, 6, 1])
# if {6,3} is selected, {5,6,1} is filtered out in the next step, meaning edges 1 and 5 are never used

Assuming I am correct in my reasoning above, this leads to a concern about scalability when there are 100 to 1,000 agents. Maybe there is an algorithm for scheduling on the fly without needing to enumerate all sets within legal_firings?

One last question: is there an easy way to resolve

to_graphviz(g, node_labels=:label, edge_labels=true)
ERROR: IOError: could not spawn `dot -Tsvg`: no such file or directory (ENOENT)

I am using CatLab 0.16.18.

1 Like

So each set in legal_firings is itself an “event”, under the assumption that if an edge can start an interaction, it will begin one as soon as possible, so legal_firings would be the entire “sample space” from which one would be chosen to update state each time an interaction completes (clock firing in the general stochastic model sense). Naively I think one would need to rerun that entire search algorithm each time state changes to ensure correctness of the legal_firings set. I’m reminded of Molloy’s work on discrete time stochastic petri nets (Discrete Time Stochastic Petri Nets | IEEE Journals & Magazine | IEEE Xplore) which spends a lot of time to consider computation of firing probabilities for firing sets (because your model allows multiple interactions to begin at the same point in time, the simultaneity of events is a shared complication with discrete time models).

Indeed it could be a problem for scalability, but calculating the legal firing sets is a hard problem in stochastic models where multiple atomic events may be scheduled simultaneously. Although, there is probably a ton of opportunities for clever algorithmic development here, such as clever caching, only updating sets that would be known to change based on which interaction just finished, etc. I think it could be made quite performant with some thought. Maybe you might find some inspiration in the “dependency graphs” used in chemical simulation (https://pubs.acs.org/doi/full/10.1021/jp993732q).

Ah, yeah, Catlab uses Graphviz (https://graphviz.org/) to produce the graphics. I think you’d need to install it with a package manager (Homebrew if on mac) on your machine, or manually if you are on Windows. There’s apparently a graphviz_jll package but I’ve never gotten it working correctly, I think the preferred way is to directly install the software.

1 Like

@slwu89, the articles above look very useful. What you provided above was a very good starting point. I appreciate all of your help!

1 Like