Agents.jl: Selecting specific neighbours (biased random walks)

Created on 25 Nov 2020  路  23Comments  路  Source: JuliaDynamics/Agents.jl

I am trying to set up a simple exclusion process on a 2D grid space where the jumps to neighboring cells are only allowed when unoccupied and the rates are asymmetric (right, up, left, and down moves do not have the same probability and depend on an internal variable "angle" of the agent).
At the moment I am having trouble identifying the appropriate neighbor node from the node_neighbors list, in particular when one of the neighbors falls on the other side of the domain (due to periodicity).

I am attaching my agent_step! function.

Thanks so much.

ps: I am starting with Julia and Agents.jl, so please excuse any silly mistakes I made in the attached code (I'd also be very keen to hear of any improvements to make the code more efficient).

agent_step.txt

question discrete

All 23 comments

Hi @mbruna!

Yes, this is a problem we've been working on previously (#294, #295) and I think has fallen by the wayside a little since we've been doing some major changes for an upcoming 4.0 release.

I'm pretty sure it's only due to the periodicity problem and may be fixed with the new GridSpace in the master branch, but I don't remember checking explicitly. Leave it with me for a bit and I'll see what I can find out.

Can I ask what version of Agents you're using? 3.7.1 or the current master branch?
(Edit: I can answer that myself. You're not using the master, since we changed node_neighbors to nearby_positions #317)

(contents of agent_step.txt)

function agent_step!(agent, model)

    # Compute probabilities of 4 spatial moves (1- right, 2- up, 3-left, 4-down)
    蟺j = [exp(cos(agent.theta)), exp(sin(agent.theta)), exp(-cos(agent.theta)), exp(-sin(agent.theta))]
    # Wiener process for the angles
    agent.theta += sqrt(2*model.DR*model.dt)*randn()

    # Move agents in space

    # check if agent will move (generate random number between 0 and 1):
    r = rand()
    # since a no-move is always the most likely, let's start with that
    if (r >= sum(蟺j))
        # no move
        return
    end

    # if I made it here I will move (provided the direction I pick is empty)
    neighbor_cells = node_neighbors(agent, model)

    # For unbiased diffusion we'd do:
    # target = rand(neighbor_cells)

    # For the biased diffusion I need to be able to select in an efficient way specific neighbors (also accounting for periodic boundary conditions)
    if r < 蟺j[1]
        # move to right
        target = # this should be the neighbour to the right of agent
    elseif r>= 蟺j[1] && r< 蟺j[1] + 蟺j[2]
        # move up
        target = # this should be the neighbour up of agent
    elseif r >= 蟺j[1] + 蟺j[2] && r < (蟺j[1] + 蟺j[2] + 蟺j[3])
        # move left
        target = # this should be the neighbour to the left of agent
    else
        # move down
        target = # this should be the neighbour down of agent
    end

    # move agent if target site is unoccupied
    if length(get_node_contents(target,model)) == 0
        move_agent!(agent, target,model)
    end
end

For me it is not clear why this is an Agents.jl related issue, so please help me out a bit. The core code we care about is:

    # For the biased diffusion I need to be able to select in an efficient way specific neighbors (also accounting for periodic boundary conditions)
    if r < 蟺j[1]
        # move to right
        target = # this should be the neighbour to the right of agent
    elseif r>= 蟺j[1] && r< 蟺j[1] + 蟺j[2]
        # move up
        target = # this should be the neighbour up of agent
    elseif r >= 蟺j[1] + 蟺j[2] && r < (蟺j[1] + 蟺j[2] + 蟺j[3])
        # move left
        target = # this should be the neighbour to the left of agent
    else
        # move down
        target = # this should be the neighbour down of agent
    end

so in principle you're asking how to find the "up" or "down" direction. Is there a bug somewhere...?

In principle you take the difference of the nearby location and the agent location. This gives you a vector. Then you compute the angle of this vector, atan(vec[2], vec[1]) which gives you the direction. What am I missing?

Based off of the discussion in #347, then no—it's not a bug, just something we could make into a quick helper function set at some stage in the future if need be.

@mbruna: for now, what it'd suggest is using something like this:

right(agent, model) =    
    (mod1(agent.pos[1]+1, model.space.dimensions[1]), agent.pos[2])

left(agent, model) =    
    (mod1(agent.pos[1]-1, model.space.dimensions[1]), agent.pos[2])

up(agent, model) =    
    (agent.pos[1], mod1(agent.pos[2]+1, model.space.dimensions[2]))

down(agent, model) =    
    (agent.pos[1], mod1(agent.pos[2]-1, model.space.dimensions[2]))

should there be 1 single function direction(agent, nearby_position, model) that gives you some kind of indicator of the nearby_pos is to the "east" or "west" or etc. of the agent?

That was essentially my question in #347, although I think you're missing the requirements slightly. We would want to know what is the grid position to the left (for example) of any given position, regardless of the boundary condition.

Rather than direction(agent, nearby_position, model) -> :west, it would be direction(agent, :west, model) -> one position left.

Sure, but :west is not useful as an output, besides the user cares to find the position "west" of the given position.

So I guess we want something like validpos(agent, :west, model).

Yes, exactly

@mbruna, I've implemented a walk! function in the current master branch (which will soon be the 4.0 release of Agents).

Since you're new to Agents, it may be best if you use the current master instead, since there will be a lot of changes for you to get accustomed to in the near future. It's quite stable at the moment, we are just adding some new features before release.

You can see updated documentation here. Since we're moving to this new version soon, I won't propagate the walk! function back to 3.7. If you're hesitant to upgrade just yet, the functions I outlined above should work for your current question regardless.

Let us know if you have any other issues.

(you add the master version by doing Pkg.add("Agents#master"))

@mbruna, I've implemented a walk! function in the current master branch (which will soon be the 4.0 release of Agents).

Since you're new to Agents, it may be best if you use the current master instead, since there will be a lot of changes for you to get accustomed to in the near future. It's quite stable at the moment, we are just adding some new features before release.

You can see updated documentation here. Since we're moving to this new version soon, I won't propagate the walk! function back to 3.7. If you're hesitant to upgrade just yet, the functions I outlined above should work for your current question regardless.

Let us know if you have any other issues.

@Libbum and @Datseris many thanks for your helpful replies and for adding the walk! function to the master branch. This is exactly what I was trying to do. I managed to move to the master branch and amend my code accordingly. I am now able to implement the agent_step! function that I needed, but I have one further question to make the code simpler and more efficient. The walk! function with directions would work fine in my problem if I did allow more than one agent per lattice site. But I only want to allow the move if the site is empty. What would be the best way to get around this?

At the moment I am not using the new walk! function. Instead I have

right(agent, model) =    
    (mod1(agent.pos[1]+1, size(model.space)[1]), agent.pos[2])

left(agent, model) =    
    (mod1(agent.pos[1]-1, size(model.space)[1]), agent.pos[2])

up(agent, model) =    
    (agent.pos[1], mod1(agent.pos[2]+1, size(model.space)[2]))

down(agent, model) =    
    (agent.pos[1], mod1(agent.pos[2]-1, size(model.space)[2]))

and then I amended the if loop I had to

    if r < 蟺j[1]
        # move to right
        target = right(agent,model)
    elseif r>= 蟺j[1] && r< 蟺j[1] + 蟺j[2]
        # move up
        target = up(agent,model)
    elseif r >= 蟺j[1] + 蟺j[2] && r < (蟺j[1] + 蟺j[2] + 蟺j[3])
        # move left
        target = left(agent,model)
    else
        # move down
        target = down(agent,model)
    end

    # move agent if target site is unoccupied
    if length(ids_in_position(target,model)) == 0
        move_agent!(agent, target,model)
    end

I tried to define a function similar to your walk!() function but adding a condition that the move is only executed if the target site is empty, but I had lots of errors to do with the directions type. I was also wondering if there would be a neater way to input my condition on the direction to take (at the moment you have a very neat way of doing this when the target is chosen at random amongst empty sites).

Thanks again!

We'll look at extending walk! a little better in #352 to include an empty check. For the current implementation, you can extend your directional functions like this:

function right(agent, model)
    new = (mod1(agent.pos[1]+1, size(model.space)[1]), agent.pos[2])
    isempty(new, model) ? new : agent.pos
end

So that target = right(agent, model) will return the step to the right if there are no other agents there, or the current agent position if the position is occupied.

Another way could be something like this:

function right_if_empty!(agent, model)
    new = (mod1(agent.pos[1]+1, size(model.space)[1]), agent.pos[2])
    if isempty(new, model)
        agent.pos = new
    end
end

Which would modify the agent position directly, only if the position on the right is empty.

I've just seen the open case #352 about making the walk! function take in a vector instead of north/south/etc, plus a clause that only makes the move if the target position is empty. That would work very well for my current problem and the extensions I am thinking about. I will keep an eye on the new commits, and for now I will use your suggestion above which is neater than what I had. Thanks again!

The new walk! implementation is now in master, which should work for your case now!

If you're happy staying with the right, left functions you've written: I have given you an example that's not 100% complete.

Setting agent.pos = new will change the position of the agent itself, but not update the position of the agent in the space. It's better to use move_agent!(agent, new, model) instead of that line.

The new walk! implementation is now in master, which should work for your case now!

great, I will try this out now.

If you're happy staying with the right, left functions you've written: I have given you an example that's not 100% complete.

Setting agent.pos = new will change the position of the agent itself, but not update the position of the agent in the space. It's better to use move_agent!(agent, new, model) instead of that line.
yes, thanks, that's how I had done it already: (say the right move was selected)
target = right(agent,model)
move_agent!(agent, target, model)

One slightly off topic question I have (when pulling the master branch). I get the following:

(@v1.4) pkg> add Agents#master
   Updating git-repo `https://github.com/JuliaDynamics/Agents.jl.git`
  Resolving package versions...
julia version requirement for package `Agents [46ada45e]` not satisfied
   Updating `~/.julia/environments/v1.4/Project.toml`
  [46ada45e] ~ Agents v3.7.1 #master (https://github.com/JuliaDynamics/Agents.jl.git)
   Updating `~/.julia/environments/v1.4/Manifest.toml`
  [46ada45e] ~ Agents v3.7.1 #master (https://github.com/JuliaDynamics/Agents.jl.git)

but I am still able to run things. Do you know why do I get this message?

You're using the global environment, so it's probable that you have a package that depends on Agents there too (my guess is AgentsPlots?) which is causing that warning.

Since the master branch is not changing anything concerning plotting, I don't think you'll have an problem at all there, but if so, open up another issue and we can track the conflict a bit more.

As an aside, have a read through this page about environments. Generally it's best to separate your workspace for different projects once you get involved with more than one Julia package. @Datseris has also written DrWatson, which can help manage environments and workflows—could be of benefit to you down the line.

Thanks for this, I will have a look. But I am not sure how this would help in my case since you are right, I am using AgentsPlots (in the same notebook, to plot the system configurations). So if I understand this correctly, even using environments I would have Agents and AgentsPlots in the same one?
I am already using DrWatson for this project (decided to start from scratch like this), but for now I was testing Agents using a notebook and including all the packages required at the top of the notebook instead of using the DrWatson approach to packages.

On a separate matter, after pulling master again and trying walk! I get the following error:


MethodError: no method matching walk!(::ActiveAgent, ::Tuple{Int64,Int64}, ::AgentBasedModel{ActiveAgent,GridSpace{2,true},typeof(random_activation),Dict{Symbol,Real}})
Closest candidates are:
  walk!(::AbstractAgent, !Matched::Type{#s75} where #s75<:Agents.Direction, ::AgentBasedModel{#s74,#s73,F,P} where P where F where #s73<:GridSpace{2,true} where #s74<:AbstractAgent) at /Users/mbruna/.julia/packages/Agents/ne9rq/src/spaces/utilities.jl:96
  walk!(::AbstractAgent, !Matched::Type{#s72} where #s72<:Agents.Direction, ::AgentBasedModel{#s71,#s55,F,P} where P where F where #s55<:GridSpace{2,true} where #s71<:AbstractAgent, !Matched::Int64) at /Users/mbruna/.julia/packages/Agents/ne9rq/src/spaces/utilities.jl:96
  walk!(::AbstractAgent, !Matched::Type{#s75} where #s75<:Agents.Direction, !Matched::AgentBasedModel{#s74,#s73,F,P} where P where F where #s73<:GridSpace{2,false} where #s74<:AbstractAgent) at /Users/mbruna/.julia/packages/Agents/ne9rq/src/spaces/utilities.jl:110

what am I doing wrong? I tried to look through the new doc and your tests and you seemed to be using walk! like this (giving it a tuple for the direction).

If you have

mutable struct ActiveAgent
...
end

this needs to be a subtype of AbstractAgent, so change it to:

mutable struct ActiveAgent <: AbstractAgent
...
end

This won't do it as it's already what I had

mutable struct ActiveAgent <: AbstractAgent
    id::Int # The identifier number of the agent
    pos::Tuple{Int,Int} # The x, y location of the agent on a 2D grid
    theta::Float64 # The direction of the agent
end

OK, I'll need some more details. Can you paste your current script? I'll take a closer look.

Another possible issue, is that you need to actually update your local version. In the REPL, type ] to get into the package management mode, then type up. Restart the REPL and try to run your script again.

You were right, the update sorted it. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dunefox picture dunefox  路  11Comments

Datseris picture Datseris  路  5Comments

Libbum picture Libbum  路  5Comments

ndgnuh picture ndgnuh  路  8Comments

fbanning picture fbanning  路  10Comments