Help with implementing a toy model using continuous space

Hello everyone! I’m new to Julia so apologies if I couldn’t figure out to make this more minimal.

This is a simple continuous space where a few dolphins (agents with type dolphin) get randomly positioned and at each model step, a hydrophone (agent with type hydrophone) checks if any dolphins are within detection range. In an interactive plot, I can step through and watch the hydrophone position change color if it could detect dolphins at that step along with the number.

The code tries to follow some of the examples in the documentation…operative word tries.

using Agents
using Random
using InteractiveDynamics
using GLMakie # use for interactive plots

# Define 2 types of agents
@agent SoundAgent ContinuousAgent{2} begin
    agent_type::Symbol  # dolphin, hydrophone
    detect::Int64
end

const v0 = (0.0, 0.0)
Dolphin(id, pos, speed, detect) = SoundAgent(id, pos, v0, :dolphin, 0)
Hydrophone(id, pos, speed, detect) = SoundAgent(id, pos, v0, :hydrophone, 0)

# Initialize model with a single hydrophone at (10,10) and 4 dolphins 
# placed randomly in a 50x50 continuous space
function initialize_model(;
    n_hydrophones = 1,
    n_dolphins = 4,
    extent = (50.,50.),
    seed = 314
    )
    
    space2d = ContinuousSpace(extent; spacing = 1.0)
    model = ABM(SoundAgent, space2d; rng = MersenneTwister(seed))

    # add 4 dolphins at random locations in space
    for _ in 1:n_dolphins
        add_agent_pos!(
            Dolphin(
                nextid(model),
                Tuple(rand(model.rng, 2)*50),
                1.0,
                0,
            ),
            model,
        )
    end

    # add a single hydrophone at position (10,10)
    for iter in 1:n_hydrophones
        add_agent_pos!(
            Hydrophone(
                nextid(model),
                (10,10),     #(0+(10*iter),50),
                1.0,
                0,
            ),
            model,
        )

    end
    return model
end

# The step should move dolphins to a new random location
# For the hydrophone, it should could count how many dolphins are in
# range (detect_dist) and place in the detect parameter.
function agent_step!(soundAgent, model)
    if (soundAgent.agent_type == :dolphin)
          move_agent!(soundAgent, model)
    end

    if (soundAgent.agent_type == :hydrophone)
        detect_dist = 10
        soundAgent.detect = 0
        for detects in nearby_agents(soundAgent, model, detect_dist)
            soundAgent.detect += 1
        end 
    end
end

# setup model
model = initialize_model()

# define colors for agents
dolph_colors(a) = 
    if a.agent_type == :dolphin
        :blue 
    elseif a.detect > 0
        :green 
    else
        :red 
    end

# Provide a interactive plot to step through iterations.  The hydrophone will
# be red if no dolphins are within detection range and green if there are.
fig, ax, abmobs = abmplot(model; agent_step!, ac = dolph_colors)

The problem seems to be that the hydrophone is counting detected dolphins from the previous model state. For example, at model step (iteration) 4, a dolphin should have been detected but was not. It should have detected 3 (ids 1,3,4) as being within 10 units. There was, however, one dolphin in range on the previous step. Here are the agents at step 4:

julia> allagents(model)
ValueIterator for a Dict{Int64, SoundAgent} with 5 entries. Values:
  SoundAgent(5, (10.0, 10.0), (0.0, 0.0), :hydrophone, 0)
  SoundAgent(4, (5.919623789660822, 13.241116752719062), (0.0, 0.0), :dolphin, 0)
  SoundAgent(2, (39.78362830054856, 28.564867667191795), (0.0, 0.0), :dolphin, 0)
  SoundAgent(3, (6.657882618425082, 15.05991173602027), (0.0, 0.0), :dolphin, 0)
  SoundAgent(1, (16.000993451695855, 11.006201657793657), (0.0, 0.0), :dolphin, 0)

The next iteration, step 5, should not have detected any, but shows 3 (which would have been correct for the previous step). The agents at step 5:

julia> allagents(model)
ValueIterator for a Dict{Int64, SoundAgent} with 5 entries. Values:
  SoundAgent(5, (10.0, 10.0), (0.0, 0.0), :hydrophone, 3)
  SoundAgent(4, (15.445609063574539, 49.81171178204833), (0.0, 0.0), :dolphin, 0)
  SoundAgent(2, (24.674884111932737, 11.439336986166392), (0.0, 0.0), :dolphin, 0)
  SoundAgent(3, (22.61210342173715, 37.24248507427378), (0.0, 0.0), :dolphin, 0)
  SoundAgent(1, (32.77698273157941, 1.1235099004958227), (0.0, 0.0), :dolphin, 0)

Would anyone be able to tell me where I went wrong or misunderstood a concept (Julia or Agents.jl specific)? I read through

Thank you!
(PS…is there a way to format this question more efficiently?)

Try replacing

for detects in nearby_agents(soundAgent, model, detect_dist)
    soundAgent.detect += 1
end 

With

dolphs = [x for x in nearby_agents(soundAgent, model, detect_dist) if x.type == :dolphin]  
soundAgent.detect = length(dolphs)

Same problem occurs…what shows up in the soundAgent.detect field appears to come from the previous model iteration.

However, your suggestion fixed a different issue I was having, so thanks! :smile:

As you can see in the iterator allagents(model), agent 5 which is the hydrophone is first. i.e. it records the number of dolphins before they get to move.
The order of the calls to agent_step! is the schedule and it can be explicitly changed. Perhaps just changing the order of adding the agents (to add hydrophones first) would do the trick, but it is best to set the schedule explicitly.

Try using:

model = ABM(SoundAgent, space2d; 
                       scheduler = Schedulers.by_id, 
                       rng = MersenneTwister(seed)
)

in initialize_model.

Yes, that’s the problem. Both options worked…changing the order of adding agents or adding the schedule.

In the API, I also saw a Schedulers.ByProperty and it mentioned property could be a symbol. Would this allow me to schedule the dolphins first through the model’s property agent_type? Otherwise, I will need to be sure of what order the agents are added to the model, right? I could see this getting confusing as the model grows in complexity.

Thank you very much for this. It seems a bit obvious now :slight_smile:

Yes, I think this is the point of this feature. Essentially this can be totally customized by a function or defining another scheduler. But the ByProperty should do it.

1 Like

No, this doesn’t change anything, it is just an inferior performance version of the above. It would only matter in stepping scenarios where agents are killed or created during the stepping function of another agent, which does not happen here.

yes, the answer is the order of activation of agents, but the ByProperty scheduler is not correct here. While it did work for you by chance, that’s not its applciation scenario. Eg if all agents had a propery :weight, the ones with more weight would act first.