[Blog] Sea Urchin Movement Model (Lévy Walk + Chemical Cue Targeting)
Published:
This post documents an R simulation for sea urchin foraging: a Lévy-walk–style search that switches to directed movement when the animal enters a chemical-cue detection radius around food sources. The output is an animated GIF that tracks the path and the cue “rings”.
Overview
The core idea is:
- The urchin explores with random steps drawn from a heavy-tailed step-length distribution (a Lévy-like rule).
- When it approaches a food source (within a detection radius), it moves toward the food.
- Once it reaches food, it stops (in this implementation).
In this demo:
- Four food sources are placed at the corners of the arena.
- Each food source has a dashed circle showing its detection radius.
- When the urchin path intersects a detection circle, the model transitions into cue-following.

R code (complete)
Libraries
# Load necessary libraries
library(ggplot2)
library(ggforce)
library(ggimage)
library(gganimate)
library(scales)
Helper functions
Distance calculation
# Function to calculate distance between two points
calculate_distance <- function(x1, y1, x2, y2) {
sqrt((x2 - x1)^2 + (y2 - y1)^2)
}
Line–circle intersection (cue detection)
This checks whether a movement segment crosses a food-source detection circle.
# Function to check if a line segment intersects with a circle
line_circle_intersection <- function(x1, y1, x2, y2, cx, cy, r) {
dx <- x2 - x1
dy <- y2 - y1
fx <- x1 - cx
fy <- y1 - cy
a <- dx^2 + dy^2
b <- 2 * (fx * dx + fy * dy)
c <- fx^2 + fy^2 - r^2
discriminant <- b^2 - 4 * a * c
if (discriminant >= 0) {
t1 <- (-b + sqrt(discriminant)) / (2 * a)
t2 <- (-b - sqrt(discriminant)) / (2 * a)
if ((t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1)) {
return(TRUE)
}
}
return(FALSE)
}
Lévy-walk movement with cue-following + stopping
Mechanism:
- Find the closest food source.
- If distance is larger than the detection threshold:
- take a Lévy-like random step (heavy-tailed step length + random angle).
- If within detection radius:
- move directly toward the food.
- if the path intersects the cue circle, snap to the food and stop.
simulate_urchin_movement_towards_food_stopping <- function(
num_steps,
alpha,
food_points,
start_x = 0,
start_y = 0,
distance_thresholds = 5
) {
x <- numeric(num_steps + 1)
y <- numeric(num_steps + 1)
x[1] <- start_x
y[1] <- start_y
reached_food <- FALSE
for (i in 2:(num_steps + 1)) {
if (!reached_food) {
closest_distance_to_food <- Inf
closest_food_index <- 0
# find closest food
for (j in 1:nrow(food_points)) {
food_x <- food_points[j, "x"]
food_y <- food_points[j, "y"]
direction_x <- food_x - x[i - 1]
direction_y <- food_y - y[i - 1]
distance_to_food <- sqrt(direction_x^2 + direction_y^2)
if (distance_to_food < closest_distance_to_food) {
closest_distance_to_food <- distance_to_food
closest_food_index <- j
}
}
# far from food -> Lévy-like random step
if (closest_distance_to_food > distance_thresholds) {
step_length <- abs(rnorm(1, mean = 0, sd = 1))^(-1 / alpha)
step_angle <- runif(1, 0, 2 * pi)
new_x <- x[i - 1] + step_length * cos(step_angle)
new_y <- y[i - 1] + step_length * sin(step_angle)
# keep inside arena bounds
x[i] <- pmin(300, pmax(-300, new_x))
y[i] <- pmin(300, pmax(-300, new_y))
} else {
# within cue radius -> move toward food
food_x <- food_points[closest_food_index, "x"]
food_y <- food_points[closest_food_index, "y"]
# if the segment intersects the cue circle, snap to food and stop
if (line_circle_intersection(
x[i - 1], y[i - 1],
food_x, food_y,
food_x, food_y,
distance_thresholds
)) {
x[i] <- food_x
y[i] <- food_y
reached_food <- TRUE
} else {
unit_direction_x <- (food_x - x[i - 1]) / closest_distance_to_food
unit_direction_y <- (food_y - y[i - 1]) / closest_distance_to_food
new_x <- x[i - 1] + unit_direction_x
new_y <- y[i - 1] + unit_direction_y
x[i] <- pmin(300, pmax(-300, new_x))
y[i] <- pmin(300, pmax(-300, new_y))
}
}
} else {
# already reached food -> stop
x[i] <- x[i - 1]
y[i] <- y[i - 1]
}
}
return(data.frame(x = x, y = y, step = 1:(num_steps + 1)))
}
Parameters and run
# Parameters for simulation
num_steps <- 3000 # Number of steps for the simulation
alpha <- 1.5 # Levy distribution parameter (alpha > 1 for heavy-tailed distribution)
food_locations <- data.frame(
x = c(-200, -200, 200, 200), # X-coordinates of the food points
y = c(-200, 200, 200, -200) # Y-coordinates of the food points
)
distance_thresholds <- 100
# Simulate sea urchin movement with stopping condition
urchin_movement_stopping <- simulate_urchin_movement_towards_food_stopping(
num_steps,
alpha,
food_locations,
distance_thresholds = distance_thresholds
)
Plot + animate (gganimate)
Notes
- Put your urchin PNG (e.g.,
sea_urchin2.png) in the working directory, or provide a full path. Download a sample image from here. - The output GIF is saved as
output.gif.
# Create ggplot object for initial plot with food points and sea urchin movement
sea_urchin_image <- "sea_urchin2.png" # Replace with your image file path
p <- ggplot() +
geom_rect(
data = food_locations,
aes(xmin = x - 0.25, xmax = x + 0.25, ymin = y - 0.25, ymax = y + 0.25),
fill = "blue"
) +
geom_line(
data = urchin_movement_stopping,
aes(x = x, y = y),
color = "blue",
alpha = 0.3
) +
geom_point(
data = food_locations,
aes(x = x, y = y),
shape = 17,
size = 5,
color = "green"
) +
geom_image(
data = urchin_movement_stopping,
aes(x = x, y = y, image = sea_urchin_image),
size = 0.15
) +
lapply(distance_thresholds, function(threshold) {
geom_circle(
data = food_locations,
aes(x0 = x, y0 = y, r = threshold),
inherit.aes = FALSE,
color = "black",
linetype = "dashed",
alpha = 0.5
)
}) +
labs(
x = "X-axis",
y = "Y-axis",
title = "Sea Urchin Searching for Chemical Cue and Food"
) +
transition_reveal(step) +
theme_bw() +
xlim(-300, 300) +
ylim(-300, 300) +
coord_fixed()
p
# Render and save animation
animation <- gganimate::animate(
p,
renderer = gganimate::gifski_renderer(),
width = 800,
height = 600
)
gganimate::anim_save("output.gif", animation)
Limitations (current implementation)
- Fixed duration: the simulation runs for a fixed
num_stepsand then ends, even if no food is reached. - Idealized behavior: cue-following is simplified; real urchin movement can be affected by currents, substrate complexity, and multiple sensory cues.
- Single stopping rule: once food is reached, the urchin stops. You could extend this to allow continued foraging among multiple patches.