JuliaDynamics / Agents.jl

Agent-based modeling framework in Julia
https://juliadynamics.github.io/Agents.jl/stable/
MIT License
765 stars 125 forks source link

New function `randomwalk!` to replace `walk!(agent, rand, model)`. #644

Closed Datseris closed 1 year ago

Datseris commented 2 years ago

I think our walk!(agent, rand, model) is an unintuitive function, with also less power than possible. When I think of a random walk, I would think that I provide a radius to a function, and the agent takes a step with distance as much as the radius, but random direction.

Our function doesn't allow for this. So, I propose that we implement a randomwalk! function with specification:

randomwalk!(agent, model, r)

which moves the agent in distance r, respecting periodic boundary conditions, but random orientation, respecting the space metric. So, for continuous space this is literally moving in a random direction in the unit circle. For Discrete space this uses the spaces metric and finds all points that have distance floor(r). For Chebyshev this is hypercube, and so on. Only for Manhattan and Chebyshev this function can utilize points that have exactly floor(r) distance. For euclidean metric this can only be approximate.

karmenlu commented 2 years ago

Interested in taking this. Haven't contributed to this repository before. Are there utility functions that would be useful for implementation? Also, does this repository contain everything I need to execute / get a feel for walk! before starting randomwalk!?

Datseris commented 2 years ago

Great! Answer: yes to everything!

Internal function you will need is move_agent!(agent, pos, model). So, the randomwalk generates the new position and then calls move_agent!.

There is only one thing that will be tricky: in this random walk, you need to collect the possible positions nearby an agent that have distance r. We don't hjave code for this at the moment. We only have code for collecting nearby positions that are within distance r. This means, that you get positions that also have less than r distance. The code is here:

https://github.com/JuliaDynamics/Agents.jl/blob/main/src/spaces/grid_general.jl#L48

You'd need to create an alternative version called offsets_at_radius that only returns position offsets that are only at distance r, not less than it.

Datseris commented 2 years ago

Update here: the internal function you will need is just walk!. You create the new direction and simply call walk!(agent, new_direction, model).

karmenlu commented 2 years ago

Thanks for the update @Datseris

mastrof commented 2 years ago

If the intention is to have useful general-use functions, random walks in ContinuousSpace allow for more freedom than just uniform random directions. It is rather easy to implement correlated random walks (i.e., the new step direction is dependent on the last one): instead of picking a new direction uniformly, one needs to apply a rotation to the current direction (Rotations.jl makes it quite easy). The correlation between successive steps is defined by the properties of the rotation matrix; as an extreme example, there are some bacteria that do perform random walks, but can only make almost-perfectly anticorrelated steps ("run-reverse"), which could be represented by rotations with angles normally distributed around 180 degrees with a small stdev.

How to smoothly fit this in the interface is another question, but since I'm already working on these things I might think about this a bit if there is interest in providing these functionalities out of the box. In 2 dimensions it will be sufficient to provide a distribution for the angle (e.g. from Distributions.jl); in 3 dimensions things can get more complicated, but if we are just working with "dimensionless" agents (not solid bodies) then it's sufficient to specify two angle distributions (in-plane and out-of-plane rotation, or yaw and pitch if you want). The simple uncorrelated random walk would be just the (default) case of Uniform angle distributions.

Datseris commented 2 years ago

we can allow a keyword argument to randomwalk! which would be tthe angle distribution for continuous space. By default, it is Uniform(0, 2π) from Distributions.jl.

Datseris commented 2 years ago

Would be nice if someone is going to implement this, sounds like a generally useful feature!

mastrof commented 2 years ago

I'm rather busy now, but if noone else takes care of this before mid november I'll be happy to help. I guess it makes sense to first implement the randomwalk! function and then wait until #673 is merged to think about rotations (since Rotations.jl works with SVectors)

Datseris commented 2 years ago

I wouldn't wait for #673 because its a breaking change. Converting to SVector is free, you can still use Rotations.jl.

mastrof commented 1 year ago

I am starting to work on this.

For the correlated random walks, agent.vel needs to be updated after each reorientation: 1 - is agent.vel = new_direction a safe / correct way to update the velocity of the agent or is there a dedicated function? 2 - should I assume that norm(agent.vel) ≈ 1 always holds, so that I can just rotate the unit vectors and then walk the agent as walk!(agent, new_direction .* r, model), or should the step size r be included in vel? I personally prefer the second option (and that's what I usually do in my simulations) which is more flexible although it requires an extra "normalization" step at each reorientation

Datseris commented 1 year ago

agent.vel = new_direction is correct, and dont assume normal velocity vectors. they typically have units related to space size.

mastrof commented 1 year ago

Shall we agree on a convention for euclidean spaces? If I had to choose, I would round the step to the on-grid distance which is closest to the given r. But I don't really work with walks on grids so I'm not sure whether this can have some strange effects or if there is a different preferred convention. Indeed small changes in r would affect the geometric properties of the walk.

E.g. consider the following 2D cube of offsets (be it hypercube)

(-2, -2)  (-2, -1)  (-2, 0)  (-2, 1)  (-2, 2)
(-1, -2)  (-1, -1)  (-1, 0)  (-1, 1)  (-1, 2)
(0, -2)   (0, -1)   (0, 0)   (0, 1)   (0, 2)
(1, -2)   (1, -1)   (1, 0)   (1, 1)   (1, 2)
(2, -2)   (2, -1)   (2, 0)   (2, 1)   (2, 2)

for which the euclidean distances are (hypercube_distances)

 2.82843  2.23607  2.0  2.23607  2.82843
 2.23607  1.41421  1.0  1.41421  2.23607
 2.0      1.0      0.0  1.0      2.0
 2.23607  1.41421  1.0  1.41421  2.23607
 2.82843  2.23607  2.0  2.23607  2.82843

suppose I have specified r=2.1. I would do something like

_, k = findmin(abs.(hypercube_distances .- r))
R = hypercube_distances[k]
offsets = hypercube[hypercube_distances .== R]

which in this case gives the offsets with distance 2.0

 CartesianIndex(0, -2)
 CartesianIndex(-2, 0)
 CartesianIndex(2, 0)
 CartesianIndex(0, 2)

But if instead I give r=2.12 it would select for the offsets at distances 2.23607 returning

 CartesianIndex(-1, -2)
 CartesianIndex(1, -2)
 CartesianIndex(-2, -1)
 CartesianIndex(2, -1)
 CartesianIndex(-2, 1)
 CartesianIndex(2, 1)
 CartesianIndex(-1, 2)
 CartesianIndex(1, 2)
Datseris commented 1 year ago

Shall we agree on a convention for euclidean spaces?

I would not bother implementing this. assuming you mean euclidean metric on a discrete space. I wouldn't care about this now. Instead I would make a random walk on chebyshev space, on manhattan space, and a random walk on ContinuousSpace (which just chooses a random angle and radius r). For Euclidean metric on DiscreteSpace it erros saying "its better to use continuous space for this as the walk properties depend on r.