Interacting_pairs() for EventQueueABM

Hello,

I’m trying to modify interacting_pairs() function for EventQueueABM model which only looks for true pairs. As EventQueueABM doesn’t have Scheduler. So, I replaced it with allids(model). Now this does give pairs but issue is Im only getting pairs with same property. So, if there are two agents with some symbol lets say :a and :b which are floating around in space. interacting_pairs() always finds pair with either :a, :a or :b, :b.

Here is my code:


function nearest_neighbor_new(agent::AbstractAgent, model::ABM{<:ContinuousSpace}, r)
    n = nearby_ids_exact(agent.pos, model, r)
    d, j = Inf, 0
    for id in n
        dnew = euclidean_distance(agent.pos, model[id].pos, model)
        if dnew < d
            d, j = dnew, id
        end
    end
    if d == Inf
        return nothing
    else
        return model[j]
    end
end


function interacting_pairs(
    model::EventQueueABM{<:ContinuousSpace},
    r::Real,
    exact = true,
)
    pairs = Tuple{Int,Int}[]
    true_pairs!(pairs, model, r)
  
    return PairIterator(pairs, getfield(model, :agents))
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_new(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

actual code for function which is restricted to StandardABM:

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