Workspace setup

library(CircStats)
library(Rcpp)
library(RcppArmadillo)
library(sp)
library(geosphere)
library(maps)
library(raster)
library(spatstat)
library(sf)
library(geoR)
library(ggplot2)
set.seed(1234)

Habitat simulation

The simulation domain was defined as a 2,500 km\(^2\) area (50 \(\times\) 50 km), discretised into a regular grid with a spatial resolution of 100 m. Habitat suitability was represented as a binary chequerboard pattern consisting of alternating 2.5 \(\times\) 2.5 km cells, classified as suitable (value = 1) and unsuitable (value = 0).

# Define grid in meters: 100 m resolution over 50 km
x_grid <- seq(0, by = 100, length.out = 500)  # 0 to 50,000 meters
y_grid <- seq(0, by = 100, length.out = 500)

# Create coordinate grid
coord_df <- expand.grid(x = x_grid, y = y_grid)

# Checkerboard pattern with 2.5 km squares
x_band <- floor(coord_df$x / 2500) %% 2
y_band <- floor(coord_df$y / 2500) %% 2


df <- data.frame(
  x = coord_df$x,
  y = coord_df$y,
  value = (x_band == y_band) + 0
)

df$value <- (df$value - min(df$value)) / (max(df$value) - min(df$value))

cov_raster <- rasterFromXYZ(df)
crs(cov_raster) <- "+proj=utm +zone=30 +datum=WGS84 +units=m +no_defs"

# Save cov_raster
save(cov_raster, file = "cov_raster.RData")

ggplot(df, aes(x = x, y = y, fill = value)) +
  geom_tile() + 
  labs(fill = "Value") + 
  xlab("X coordinates")+
  ylab("Y coordinates") +
  scale_fill_viridis_c()+
  coord_fixed(expand = FALSE) + 
  theme_bw() 

Movement simulation

Behavioural states

Animal movement was modelled using a three-state behavioural framework designed to capture the main movement modes of non-central place foragers, which do not undertake directed trips to or from a central location. Specifically, State 1 (travelling) represents fast, directed movement; State 2 (search) corresponds to moderately fast movement with some directional persistence; and State 3 (foraging) reflects slower, more tortuous movement associated with intensive resource use. Transitions between behavioural states were influenced by habitat conditions, such that individuals were more likely to enter search (State 2) or foraging (State 3) when in suitable habitat, and more likely to leave the foraging state as habitat suitability decreased or as time increased.

State transition model

Transitions between behavioural states were governed by a probabilistic matrix informed by Euclidean distance to the colony, time since departure from the nest, and habitat suitability. Transition probabilities were computed using a multinomial logit formulation, with parameters chosen to reflect biologically realistic behaviour—for example, an increased probability of switching to foraging in suitable habitat (Michelot et al. 2017).

\[ T = \begin{pmatrix} 1 - p_{12} & p_{12} & 0 \\ 0 & 1 - p_{23} & p_{23} \\ r_{31} & r_{32} & r_{33} \end{pmatrix} \]

where \(p_{12}\) corresponds to switching from travelling \(\rightarrow\) searching, \(p_{23}\) searching \(\rightarrow\) foraging and \(r_{31}, r_{32}, r_{33}\) the transitions from foraging to other states.

We defined:

\[p_{12} = \text{logit}^{-1}(\eta_{12})\] \[p_{23} = \text{logit}^{-1}(\eta_{23})\] \[r_{31} = \frac{e^{\eta_{31}}}{1 + e^{\eta_{31}} + e^{\eta_{32}}}\] \[r_{32} = \frac{e^{\eta_{32}}}{1 + e^{\eta_{31}} + e^{\eta_{32}}}\] \[r_{33} = 1 - r_{31} - r_{32}\]

The linear predictors were specified as:

\[ \eta_{12} = -2 + 1.5 \cdot \text{Habitat} \]

\[ \eta_{23} = -1.5 + 3 \cdot \text{Habitat} \]

\[ \eta_{31} = -3 - 3 \cdot \text{Habitat} + 0.05 \cdot \text{Time} \]

\[ \eta_{32} = -3 - 1.5 \cdot \text{Habitat} + 0.05 \cdot \text{Time} \]

# -------------------------------------------------------------
# Transition matrix
# ------------------------------------------------------------
addLogit_rcpp <- function(x1, x2) {
  p1 <- exp(x1)
  p2 <- exp(x2)
  res <- c(p1 / (1 + p1 + p2), p2 / (1 + p1 + p2), 1 - (p1 / (1 + p1 + p2)) - (p2 / (1 + p1 + p2)))
  return(res)
}

trMatrix_rcpp <- function(beta, time, cov) {
  trMat <- matrix(ncol = 3, nrow = 3)
  
  # From State 1
  p12 <- plogis(beta[1] + beta[2] * cov)
  trMat[1, 1] <- 1 - p12
  trMat[1, 2] <- p12
  trMat[1, 3] <- 0
  
  # From State 2
  p23 <- plogis(beta[3] + beta[4] * cov)
  trMat[2, 1] <- 0
  trMat[2, 2] <- 1 - p23
  trMat[2, 3] <- p23
  
  # From State 3
  p31 <- beta[5] + beta[6] * cov + beta[7] * time
  p32 <- beta[8] + beta[9] * cov + beta[10] * time
  r3 <- addLogit_rcpp(p31, p32)
  
  
  trMat[3, 1] <- r3[1]
  trMat[3, 2] <- r3[2]
  trMat[3, 3] <- r3[3]
  
  return(trMat)
}

# HMM transition coefficients
beta <- c(
  -2, 1.5,        # State 1 → 2 
  -1.5, 3,      # State 2 → 3 
  -3, -3, 0.05,  # State 3 → 1 (less likely to leave)
  -3, -1.5, 0.05  # State 3 → 2 (less likely to leave)
)

Higher habitat suitability increased the probability of transitioning into search and foraging states, whereas the likelihood of leaving the foraging state increased with time spent within a patch. This transition structure captures realistic patch-use dynamics typical of non-central place foragers.

State-specific parameters

State-specific behaviours process were modelled as correlated random walks, with associated parameters varying among behavioural states. Step lengths and turning angles were modelled using gamma and von Mises distributions, respectively, with state-specific parameter values.

State Mean step length (m) SD (m) von Mises \(\kappa\) Turning angle mean
1 (Travel) 571.8 195.4 14 0
2 (Search) 350.0 190.0 5 \(\pi\)
3 (Foraging) 108.0 131.2 0.84 0

New locations were then calculated from sampled step lengths and turning angles using great-circle geometry implemented in the R package geosphere (Hijmans 2026).

mu <- c(571.8, 350, 108)
sigma <- c(195.4, 190, 131.2)
am <- c(0, -0.00647766, pi)
kappa <- c(8, 5, 0.84)
nbStates <- 3

Step selection

At each time step, we drew 10,000 candidate steps from the movement kernel associated with the current behavioural state. We calculated a relative selection weight for each candidate by exponentiating the linear predictor and then selected one end point probabilistically as the animal’s next location. Each candidate step \(i\) was evaluated using a log‑linear model of the form:

\[ \log(w_i) = \beta_1 \times L_i + \beta_2 \times \log L_i + \beta_3 \times h_i \]

\[ w_i = \exp(\eta_i) \quad \mathrm{Pr}(i) = \frac{\exp(\eta_i)}{\sum_j \exp(\eta_j)} \]

where \(L_i\) is the step length, \(h_i\) is the habitat covariate and \(\beta_1, \beta_2\) and \(\beta_3\) are the corresponding selection coefficients.

The coefficients for step length and log step length were derived analytically from the gamma distribution parameters (Klappstein, Thomas, and Michelot 2023; Avgar et al. 2016) while the habitat coefficient \(\beta_3\) was varied systematically across simulations with values ranging from 0.5 to 1.5 (in 0.25 increments) as well as 0 (no habitat selection) to assess how models performed under different selection strengths.

Individuals were initialised at a random location within the simulation area at time \(t = 0\), with the first step generated by drawing a direction \(\phi_0 \sim \mathrm{von\ Mises}(0, 1)\) and a step length \(L_0 \sim \mathrm{Gamma}(k, \theta)\). The latter was parameterised in terms of the state-specific mean \(\mu\) and standard deviation \(\sigma\) for travel, such that \(k = \mu^2 / \sigma^2\) and \(\theta = \sigma^2 / \mu\). The resulting displacement defines the first location.

### SSF parameters
# Equations exist to determine the scale and shape parameters of a gamma distribution
# based on its mean and sd
scale <- sigma^2 / mu
shape <- mu^2 / sigma^2

# The equation presented in Appendix A gives the relationship between the
# gamma distribution parameters and the selection coefficients (referred to as β1 and β2 for L and log(L) respectively).
# β1 −1/scale and β2 = shape − 2.
beta1 <- -1 / scale
beta2 <- shape - 2

cov_beta <- 0 #0.5, 0.75, 1, 1.25, 1.5, 1.75,2

Simulate animal tracks

We simulate a full population of 1,000 individuals and one track per individual with duration of 360 minutes.

nbTracks <- 1
nbID <- 1000

for(c in 1:length(cov_beta)){
  
  cov_gamma <- rep(1, 3)
  cov_coefs <- cov_beta[c] * cov_gamma
  
  ssf_betas <- matrix(c(beta1, beta2, kappa, cov_coefs),
                      ncol = nbStates, byrow = TRUE)
  
  ssf_formula <- ~ cov
  n_zeros <- 10000
  
  data <- NULL
  
  for (id in 1:nbID) {
    print(paste0("Beta: ", cov_beta[c],"; Individual: ",id ))
    
    track_data <- NULL
    
    state <- 1
    phi <- rvm(1, 0, 1)
    
    len <- rgamma(1,
                  shape = mu[1]^2 / sigma[1]^2,
                  scale = sigma[1]^2 / mu[1])
    
    centre <- c(sample(seq(500,48500,100),1),
                sample(seq(500,48500,100),1))
    
    newLoc <- centre + c(len * cos(phi), len * sin(phi))
    
    while(!newLoc[1] >= 100 & newLoc[1] <= 49900 &
        newLoc[2] >= 100 & newLoc[2] <= 49900){
          
            centre <- c(sample(seq(500,48500,100),1),
                sample(seq(500,48500,100),1))
    
    newLoc <- centre + c(len * cos(phi), len * sin(phi))
        }
    
    angle <- rvm(1, mean = am[state], k = kappa[state])
    
    track_data <- rbind(track_data, data.frame(
      x = centre[1], y = centre[2],
      step = NA, angle = NA, bearing = NA,
      state = 1, time = 0, ID = id))
    
    time <- 0
    
    while (time < 360) {
      
      time <- time + 1
      phi0 <- phi
      
      cov <- raster::extract(cov_raster, matrix(newLoc, nrow = 1))
      trMat <- trMatrix_rcpp(beta, time, cov)
      state <- sample(1:nbStates, 1, prob = trMat[state, ])
      
      angle_candidates <- rvm(n_zeros, mean = am[state], k = kappa[state])
      phi_candidates <- phi + angle_candidates
      
      len_candidates <- rgamma(n_zeros,
                               shape = mu[state]^2 / sigma[state]^2,
                               scale = sigma[state]^2 / mu[state])
      
      dx <- len_candidates * cos(phi_candidates)
      dy <- len_candidates * sin(phi_candidates)
      
      endpoints <- cbind(newLoc[1] + dx, newLoc[2] + dy)
      
      inside <- endpoints[,1] >= 100 & endpoints[,1] <= 49900 &
        endpoints[,2] >= 100 & endpoints[,2] <= 49900
      
      while (sum(inside) == 0) {
        
        # sample new turning angles centred on reverse direction
        angle_candidates <- rvm(
          n_zeros,
          mean = pi,
          k = kappa[state]   # or a separate kappa_back
        )
        
        phi_candidates <- phi + angle_candidates
        
        dx <- len_candidates * cos(phi_candidates)
        dy <- len_candidates * sin(phi_candidates)
        
        endpoints <- cbind(
          newLoc[1] + dx,
          newLoc[2] + dy
        )
        
        inside <- endpoints[,1] >= 100 & endpoints[,1] <= 49900 &
          endpoints[,2] >= 100 & endpoints[,2] <= 49900
      }
      
      endpoints <- endpoints[inside, ,drop = FALSE]
      angle_candidates <- angle_candidates[inside]
      phi_candidates <- phi_candidates[inside]
      len_candidates <- len_candidates[inside]
      
      cov_vals <- raster::extract(cov_raster, endpoints)
      
      endpoints.df <- data.frame(
        x = endpoints[,1],
        y = endpoints[,2],
        cov = cov_vals
      )
      
      mod_matrix <- model.matrix(ssf_formula, endpoints.df)
      mod_matrix <- mod_matrix[, -1, drop = FALSE]
      
      linear_pred <- mod_matrix %*% as.matrix(ssf_betas[4, state])
      ssf_prob <- exp(linear_pred)
      ssf_prob <- ssf_prob / sum(ssf_prob)
      
      chosen <- sample(seq_len(nrow(endpoints)), 1, prob = ssf_prob)
      
      newLoc <- endpoints[chosen, ]
      phi <- phi_candidates[chosen]
      len <- len_candidates[chosen]
      
      track_data <- rbind(track_data, data.frame(
        x = newLoc[1], y = newLoc[2],
        step = len,
        angle = phi - phi0,
        bearing = phi,
        state = state,
        time = time,
        ID = id
      ))
    }
    
    data <- rbind(data, track_data)
  }
  
  # Convert to sf
  data_sf <- st_as_sf(data, coords = c("x", "y"), crs = 32630)
  
  # Save only the data object
  save(data_sf, file = paste0("NCPF_cov", cov_beta[c], "_", nbID, "ind.RData"))
}

# plot tracks
ggplot() +
  geom_tile(data=df, aes(x,y,fill=factor(value)), alpha=0.1)+
  geom_path(data= data[data$ID==100,], aes(x, y, col= factor(state), group = ID)) +
  scale_fill_manual(values = c("lightgrey", "darkgreen"), name = "Habitat", labels=c("Unsuitable", "Suitable"))+
  scale_colour_manual(values = c("black", "darkorange", "darkred"), name = "Behaviours", labels=c("Travel", "Search", "Foraging"))+
  theme_bw() +
  coord_fixed()

References

Avgar, Tal, Jonathan R Potts, Mark A Lewis, and Mark S Boyce. 2016. “Integrated Step Selection Analysis: Bridging the Gap Between Resource Selection and Animal Movement.” Methods in Ecology and Evolution 7 (5): 619–30. https://doi.org/10.1111/2041-210x.12528.
Hijmans, Robert J. 2026. “Geosphere: Spherical Trigonometry.”
Klappstein, N J, L Thomas, and T Michelot. 2023. “Flexible Hidden Markov Models for Behaviour-Dependent Habitat Selection.” Movement Ecology 11 (1): 30. https://doi.org/10.1186/s40462-023-00392-3.
Michelot, Théo, Roland Langrock, Sophie Bestley, Ian D Jonsen, Theoni Photopoulou, and Toby A Patterson. 2017. “Estimation and Simulation of Foraging Trips in Land-Based Marine Predators.” Ecology 98 (7): 1932–44. https://doi.org/10.1002/ecy.1880.