Modifying interaction_pairs() for EventQueueABM

Hello everyone,

I’m trying to modify inbuild function interaction_pairs() to make it usable for EventQueueABM model too. Any suggestion will be helpful… :slight_smile:

Here is original code :

function interacting_pairs(model::ABM{<:ContinuousSpace}, r::Real, method;
        scheduler = abmscheduler(model), nearby_f = nearby_ids_exact,
    )
    @assert method ∈ (:nearest, :all, :types)
    pairs = Tuple{Int,Int}[]
    if method == :nearest
        true_pairs!(pairs, model, r, scheduler)
    elseif method == :all
        all_pairs!(pairs, model, r, nearby_f)
    elseif method == :types
        type_pairs!(pairs, model, r, scheduler, nearby_f)
    end
    return PairIterator(pairs, agent_container(model))
end

function all_pairs!(
    pairs::Vector{Tuple{Int,Int}},
    model::ABM{<:ContinuousSpace},
    r::Real, nearby_f,
)
    for a in allagents(model)
        for nid in nearby_f(a, model, r)
            # Sort the pair to overcome any uniqueness issues
            new_pair = isless(a.id, nid) ? (a.id, nid) : (nid, a.id)
            new_pair ∉ pairs && push!(pairs, new_pair)
        end
    end
end

function true_pairs!(pairs::Vector{Tuple{Int,Int}}, model::ABM{<:ContinuousSpace}, r::Real, scheduler)
    distances = Vector{Float64}(undef, 0)
    for a in (model[id] for id in scheduler(model))
        nn = nearest_neighbor(a, model, r)
        nn === nothing && continue
        # Sort the pair to overcome any uniqueness issues
        new_pair = isless(a.id, nn.id) ? (a.id, nn.id) : (nn.id, a.id)
        if new_pair ∉ pairs
            # We also need to check if our current pair is closer to each
            # other than any pair using our first id already in the list,
            # so we keep track of nn distances.
            dist = euclidean_distance(a.pos, nn.pos, model)

            idx = findfirst(x -> first(new_pair) == x, first.(pairs))
            if idx === nothing
                push!(pairs, new_pair)
                push!(distances, dist)
            elseif idx !== nothing && distances[idx] > dist
                # Replace this pair, it is not the true neighbor
                pairs[idx] = new_pair
                distances[idx] = dist
            end
        end
    end
    to_remove = Int[]
    # `counter` counts the number of occurencies for each item, it comes from DataStructure.jl
    for doubles in [k for (k,v) in counter(Iterators.flatten(pairs)) if v>1]
        # This list is the set of pairs that have two distances in the pair list.
        # The one with the largest distance value must be dropped.
        fidx = findfirst(isequal(doubles), first.(pairs))
        if fidx !== nothing
            lidx = findfirst(isequal(doubles), last.(pairs))
            largest = distances[fidx] <= distances[lidx] ? lidx : fidx
            push!(to_remove, largest)
        else
            # doubles are not from first sorted, there could be more than one.
            idxs = findall(isequal(doubles), last.(pairs))
            to_keep = findmin(map(i->distances[i], idxs))[2]
            deleteat!(idxs, to_keep)
            append!(to_remove, idxs)
        end
    end
    deleteat!(pairs, unique!(sort!(to_remove)))
end

function type_pairs!(
    pairs::Vector{Tuple{Int,Int}},
    model::ABM{<:ContinuousSpace},
    r::Real, scheduler, nearby_f,
)
    # We don't know ahead of time what types the scheduler will provide. Get a list.
    available_types = unique(typeof(model[id]) for id in scheduler(model))
    for id in scheduler(model)
        for nid in nearby_f(model[id], model, r)
            neighbor_type = typeof(model[nid])
            if neighbor_type ∈ available_types && neighbor_type !== typeof(model[id])
                # Sort the pair to overcome any uniqueness issues
                new_pair = isless(id, nid) ? (id, nid) : (nid, id)
                new_pair ∉ pairs && push!(pairs, new_pair)
            end
        end
    end
end

struct PairIterator{A}
    pairs::Vector{Tuple{Int,Int}}
    agents::Dict{Int,A}
end

Base.eltype(::PairIterator{A}) where {A} = Tuple{A, A}
Base.length(iter::PairIterator) = length(iter.pairs)
function Base.iterate(iter::PairIterator, i = 1)
    i > length(iter) && return nothing
    p = iter.pairs[i]
    id1, id2 = p
    return (iter.agents[id1], iter.agents[id2]), i + 1
end

Modified version for only nearest:

function interacting_pairs(
    model::EventQueueABM{<:ContinuousSpace},
    r::Real,
    exact = true,
)
    pairs = Tuple{Int,Int}[]
    true_pairs!(pairs, model, r)
  
    return PairIterator(pairs, agent_container(model))
end



function true_pairs!(pairs::Vector{Tuple{Int,Int}}, model::EventQueueABM{<:ContinuousSpace}, r::Real)
    distances = Vector{Float64}(undef, 0)
    for a in (model[id] for id in allids(model))
        nn = nearest_neighbor(a, model, r)
        nn === nothing && continue
        # Sort the pair to overcome any uniqueness issues
        new_pair = isless(a.id, nn.id) ? (a.id, nn.id) : (nn.id, a.id)
        if new_pair ∉ pairs
            # We also need to check if our current pair is closer to each
            # other than any pair using our first id already in the list,
            # so we keep track of nn distances.
            dist = euclidean_distance(a.pos, nn.pos, model)

            idx = findfirst(x -> first(new_pair) == x, first.(pairs))
            if idx === nothing
                push!(pairs, new_pair)
                push!(distances, dist)
            elseif idx !== nothing && distances[idx] > dist
                # Replace this pair, it is not the true neighbor
                pairs[idx] = new_pair
                distances[idx] = dist
            end
        end
    end
    to_remove = Int[]
    for doubles in symdiff(unique(Iterators.flatten(pairs)), collect(Iterators.flatten(pairs)))
        # This list is the set of pairs that have two distances in the pair list.
        # The one with the largest distance value must be dropped.
        fidx = findfirst(isequal(doubles), first.(pairs))
        if fidx !== nothing
            lidx = findfirst(isequal(doubles), last.(pairs))
            largest = distances[fidx] <= distances[lidx] ? lidx : fidx
            push!(to_remove, largest)
        else
            # doubles are not from first sorted, there could be more than one.
            idxs = findall(isequal(doubles), last.(pairs))
            to_keep = findmin(map(i->distances[i], idxs))[2]
            deleteat!(idxs, to_keep)
            append!(to_remove, idxs)
        end
    end
    deleteat!(pairs, unique!(sort!(to_remove)))
end


struct PairIterator{A}
    pairs::Vector{Tuple{Int,Int}}
    agents::Dict{Int,A}
end

Base.length(iter::PairIterator) = length(iter.pairs)
function Base.iterate(iter::PairIterator, i = 1)
    i > length(iter) && return nothing
    p = iter.pairs[i]
    id1, id2 = p
    return (iter.agents[id1], iter.agents[id2]), i + 1
end

At the moment Im getting error for agent_container. I thought it was some internal function of Agents.jl but seems like not.

Error:

ERROR: UndefVarError: `agent_container` not defined
Stacktrace:
  [1] interacting_pairs(model::EventQueueABM{…}, r::Float64, exact::Bool)
    @ Main ~/PhD/Program_Julia/code/continuous_time_ABM.jl:23
  [2] interacting_pairs
    @ ~/PhD/Program_Julia/code/continuous_time_ABM.jl:20 [inlined]
  [3] move!(agent::CELL, model::EventQueueABM{…})
    @ Main ~/PhD/Program_Julia/code/continuous_time_ABM.jl:136
  [4] process_event!(event_tuple::Tuple{…}, model::EventQueueABM{…})
    @ Agents ~/.julia/packages/Agents/8JW8b/src/simulations/step_eventqueue.jl:60
  [5] one_step!(queue::DataStructures.BinaryHeap{…}, model_t::Base.RefValue{…}, stop_time::Float64, model::EventQueueABM{…})
    @ Agents ~/.julia/packages/Agents/8JW8b/src/simulations/step_eventqueue.jl:42
  [6] step_ahead!
    @ ~/.julia/packages/Agents/8JW8b/src/simulations/step_eventqueue.jl:19 [inlined]
  [7] step!
    @ ~/.julia/packages/Agents/8JW8b/src/simulations/step_eventqueue.jl:5 [inlined]
  [8] step!(abmobs::ABMObservable{…}, t::Float64)
    @ AgentsVisualizations ~/.julia/packages/Agents/8JW8b/ext/AgentsVisualizations/src/model_observable.jl:37
  [9] (::AgentsVisualizations.var"#77#80"{Int64, EventQueueABM{…}, ABMObservable{…}, Observable{…}})(io::VideoStream)
    @ AgentsVisualizations ~/.julia/packages/Agents/8JW8b/ext/AgentsVisualizations/src/convenience.jl:123
 [10] Record(func::AgentsVisualizations.var"#77#80"{…}, figlike::Figure; kw_args::@Kwargs{…})
    @ Makie ~/.julia/packages/Makie/iRM0c/src/recording.jl:166
 [11] record(func::Function, figlike::Figure, path::String; kw_args::@Kwargs{framerate::Int64, compression::Int64})
    @ Makie ~/.julia/packages/Makie/iRM0c/src/recording.jl:148
 [12] record
    @ ~/.julia/packages/Makie/iRM0c/src/recording.jl:146 [inlined]
 [13] abmvideo(file::String, model::EventQueueABM{…}; spf::Nothing, dt::Float64, framerate::Int64, frames::Int64, title::String, showstep::Bool, figure::@NamedTuple{…}, axis::@NamedTuple{}, recordkwargs::@NamedTuple{…}, kwargs::@Kwargs{…})
    @ AgentsVisualizations ~/.julia/packages/Agents/8JW8b/ext/AgentsVisualizations/src/convenience.jl:120
 [14] top-level scope
    @ ~/PhD/Program_Julia/code/continuous_time_ABM.jl:248
Some type information was truncated. Use `show(err)` to see complete types.```

agent_container(model) = getfield(model, :agents) solves error issue.