I’ve found a (somewhat imprecise) solution that is acceptable for my use case. In short, this solution tracks the last position of each agent, and at each step checks whether there is an island in the space spanned by the previous and current positions.
This solution is great when an agent moves only along a single direction, but gets less precise the more diagonal the movement. Since the movement in my application will be quite slow and the spatial resolution fairly high, I think that these imprecisions won’t matter.
using Agents, Random, CairoMakie
@agent struct Cocos(ContinuousAgent{2,Float64})
lifetime::Int
pos_type::Int
last_pos::SVector{2, Float64}
end
function initialize(;
n_nuts=14,
lifetime=40,
extent=(150, 150),
seed=1,
)
space2d = ContinuousSpace(extent; spacing=1.0, update_vel!)
rng = Xoshiro(seed)
xdim, ydim = extent
island_matrix = zeros(Int, extent)
island_matrix[:, 100] .= 1
properties = (
current=[(0, 1) for _ in 1:xdim, _ in 1:ydim],
island_matrix=island_matrix,
)
model = StandardABM(Cocos, space2d; properties, rng, agent_step!, model_step!, scheduler=Schedulers.Randomly())
for i in 1:n_nuts
vel = (0, 0)
pos = (10i, ydim / 2)
pos_type = getindex_grid(model.island_matrix, pos)
last_pos = pos
add_agent!(pos, model, vel, lifetime, pos_type, last_pos)
end
return model
end
function agent_step!(agent, model)
if expired(agent) || onisland(agent)
remove_agent!(agent, model)
else
agent.last_pos = agent.pos
move_agent!(agent, model, 5.0)
end
return nothing
end
function model_step!(model)
update_agents!(model)
end
function update_vel!(agent, model)
agent.vel = getindex_grid(model.current, agent.pos)
end
function update_agents!(model)
for agent in allagents(model)
agent.lifetime -= 1
maybe_update_pos_type!(agent, model)
end
return nothing
end
function maybe_update_pos_type!(agent, model)
if onisland(agent, model)
agent.pos_type = in_path(model.island_matrix, agent.last_pos, agent.pos)
end
return nothing
end
function onisland(agent, model)
island_in_path = in_path(model.island_matrix, agent.last_pos, agent.pos)
return !isempty(island_in_path)
end
onisland(agent::Cocos) = agent.pos_type > 0
expired(agent::Cocos) = iszero(agent.lifetime)
truncate(x; at=1) = x < at ? 1 : x
coords(v) = truncate.(round.(Int, v, RoundUp))
getindex_grid(A, pos) = A[coords(pos)...]
sorted_bounds(a, b) = a < b ? (a, b) : (b, a)
function in_path(env, a, b)
xa, ya = coords(a)
xb, yb = coords(b)
# Exit if coordinates match after rounding
if xa == xb && ya == yb
return 0
end
x_min, x_max = sorted_bounds(xa, xb)
y_min, y_max = sorted_bounds(ya, yb)
# Search for island but error if more than one island in path
island_tile = 0
@inbounds for x in x_min:x_max
for y in y_min:y_max
current_tile = env[x, y]
if current_tile != 0
if island_tile == 0
island_tile = current_tile
elseif island_tile != current_tile
error("Two or more islands in path: [$current_tile, $island_tile]")
end
end
end
end
return island_tile
end
model = initialize()
const cocos_polygon = Makie.Polygon(Point2f[(-1, -1), (2, 0), (-1, 1)])
function cocos_marker(c::Cocos)
φ = atan(c.vel[2], c.vel[1])
rotate_polygon(cocos_polygon, φ)
end
islandcolor(model)::Matrix{Int} = model.island_matrix .> 0
heatkwargs = (colormap = [:white, :black], colorrange = (0, 1))
plotkwargs = (;
agent_marker = cocos_marker,
agent_color = :orange,
agentplotkwargs = (strokewidth = 1.0, strokecolor = :black),
heatarray = islandcolor,
heatkwargs = heatkwargs,
)
abmvideo(
"floating.mp4", model;
framerate=20, frames=15,
title="Coconuts",
plotkwargs...
)