CSmask2000 <- make.mask (CStraps, buffer = 30000,
type = "trapbuffer", spacing = 2000, poly = BLKS_BC,
poly.habitat = FALSE, keep.poly = FALSE)
Appendix F — Non-Euclidean distances
Spatially explicit capture–recapture (SECR) entails a distance-dependent observation model: the expected number of detections (\lambda) or the probability of detection (g) declines with increasing distance between a detector and the home-range centre of a focal animal. ‘Distance’ here usually, and by default, means the Euclidean distance. The observation model can be customised by replacing the Euclidean distance with one that ‘warps’ space in some ecologically meaningful way. There are innumerable ways to do this. One is the non-Euclidean ‘ecological distance’ envisioned by Royle et al. (2013). Redefining distance is a way to model spatial variation in the size of home ranges, and hence the spatial scale of movement \sigma; Efford et al. (2016) used this to model inverse covariation between density and home range size. Distances measured along a linear habitat network such as a river system are also non-Euclidean (see package secrlinear).
This document shows how to define and use non-Euclidean distances in secr. Code is included for the non-Euclidean SECR simulation of Sutherland et al. (2015).
F.1 Basics
The Euclidean distance between points (x_1,y_1) and (x_2,y_2) is given by d = \sqrt{(x_1-x_2)^2 + (y_1 - y_2)^2}. Non-Euclidean distances are defined in secr by setting the ‘userdist’ component of the ‘details’ argument of secr.fit
. The options are (i) to provide a static (pre-computed) K \times M matrix containing the distances between the K detectors and each of the M mask points, or (ii) to provide a function that computes the distances dynamically. A static distance matrix can allow for barriers to movement. Providing a function is more flexible and allows the estimation of a parameter for the distance model, but evaluating the function for each likelihood slows down model fitting.
F.2 Static userdist
A pre-computed non-Euclidean distance matrix may incorporate constraints on movement, particularly mapped barriers to movement, and this is the most obvious reason to employ a static userdist. The function nedist
builds a suitable matrix.
As an example, take the 1996 DNA survey of the grizzly bear population in the Central Selkirk mountains of British Columbia by Mowat & Strobeck (2000). Their study area was partly bounded by lakes and reservoirs that we assume are rarely crossed by bears. To treat the lakes as barriers in a SECR model we need a matrix of hair snag – mask point distances for the terrestrial (non-Euclidean) distance between each pair of points.
We start with the hair snag locations CStraps
and a SpatialPolygonsDataFrame object BLKS_BC
representing the large lakes (Fig. F.1). Buffering 30 km around the detectors gives a naive mask (Fig. F.1 b); we use a 2-km pixel size and reject points centred in a lake. The shortest dry path from many points on the naive mask to the nearest detector is much longer than the straight line distance.
code to plot Central Selkirk naive mask
par(mfrow = c(1,2), mar = c(1,4,1,4), cex = 1.6, col = "black")
plot(CStraps, gridl = FALSE, border = 30000)
selectedlakes <- c(1730, 2513, 2514, 2769, 2686, 2749)
plot(BLKS_BC[selectedlakes,], col = "blue", add = TRUE)
text(1546000, 510000, "a.", xpd = TRUE, col = "black")
plot(CStraps, gridl = FALSE, border = 30000, hidetr = TRUE)
plot(CSmask2000, dots = FALSE, add = TRUE)
plot(CStraps, add = TRUE)
par(col = "black", cex = 1.6)
text(1537000, 510000, "b.", xpd = TRUE)
We next calculate the matrix of all dry detector–mask distances by calling nedist
that in turn uses functions from the R package gdistance (van Etten, 2023). Missing pixels in the mask represent barriers to movement when their combined width exceeds a threshold determined by the adjacency rule in gdistance.
What do we mean by an adjacency rule? gdistance finds least-cost distances through a graph formed by joining ‘adjacent’ pixels. Adjacent pixels are defined by the argument ‘directions’, which may be 4 (rook’s case), 8 (queen’s case) or 16 (knight and one-cell queen moves) as in the raster function adjacent
. The default in nedist
is ‘directions = 16’ because that gives the best approximation to Euclidean distances when there are no barriers. The knight’s moves are \sqrt 5 \approx 2.24 \times cell width (‘spacing’), so the width of a polygon intended to map a barrier should be at least 2.24 \times cell width.
Some of the BC lakes are narrow and less than 4.48 km wide. To ensure these act as barriers we could simply reduce the spacing of our mask, but that would slow down model fitting. The alternative is to retain the 2-km mask for model fitting and to define a finer (0.5-km) mask purely for the purpose of computing distances1:
CSmask500 <- make.mask (CStraps, buffer = 30000, type =
"trapbuffer", spacing = 500, poly = BLKS_BC, poly.habitat =
FALSE, keep.poly = FALSE)
userd <- nedist(CStraps, CSmask2000, CSmask500)
The first argument of nedist
provides the rows of the distance matrix and the second argument the columns; the third (if present) defines an alternative mask on which to base the calculations. To verify the computation, map the distance from a chosen detector i to every point in a mask. Here is a short function to do that (example in Fig. F.2 a).
# map non-Euclidean distance from detector i
# or arbitrary point on existing plot (i = NA)
dmap <- function (traps, mask, userd, i = 1, ...) {
if (is.na(i)) i <- nearesttrap(unlist(locator(1)), traps)
covariates(mask) <- data.frame(d = userd[i,])
covariates(mask)$d[!is.finite(covariates(mask)$d)] <- NA
plot(mask, covariate = "d", ...)
points(traps[i,], pch = 3, col = "red")
}
At this point we could simply use ‘userd’ as our userdist matrix. However, CSmask2000
now includes a lot of points that are further than 30 km from any detector. It is better to drop these points and the associated columns of ‘userd’ (Fig. F.2 b):
code to plot Central Selkirk non-Euclidean distances and mask
par(mfrow = c(1,2), mar = c(1,4,1,4), cex = 1.1, col = "black")
dmap(CStraps, CSmask2000, userd, dots = FALSE, scale = 0.001,
title = "km")
par(cex = 1.6)
text(1537000, 510000, "a.", xpd=T, col="black")
plot(CStraps, gridl = FALSE, border = 30000, hidetr = TRUE)
plot(CSmask2000b, dots = FALSE, add = TRUE)
plot(CStraps, add = TRUE); par(col = "black", cex = 1.6)
text(1537000, 510000, "b.", xpd = TRUE, col = "black")
Finally, we can fit a model using the non-Euclidean distance matrix:
For completeness, note that Euclidean distances may also be pre-calculated, using the function edist
. By default, secr.fit
uses that function internally, and there is usually little speed improvement when the calculation is done separately.
F.3 Dynamic userdist
For dynamic non-Euclidean distances a function is passed to secr.fit
in the ‘details’ argument ‘userdist’, rather than a matrix. The userdist function should take three arguments. The first two are simply 2-column matrices with the coordinates of the detectors and animal locations (mask points) respectively. The third is a habitat mask (this may be the same as xy2). The function has this form:
Computation of the distances is entirely under the control of the user – here we indicate that by ‘…’. The calculations may use cell-specific values of two ‘real’ parameters ‘D’ and ‘noneuc’ that as needed are passed by secr.fit
as covariates of the mask. ‘D’ is the usual cell-specific expected density in animals per hectare. ‘noneuc’ is a special cell-specific ‘real’ parameter used only here: it means whatever the user wants it to mean.
Whether ‘noneuc’, ‘D’ or other mask covariates are needed by mydistfn is indicated by the character vector returned by mydistfn when it is called with no arguments. Thus, charactervector may be either a zero-length character vector or a vector of one or more parameter names (“noneuc”, “D”, c(“noneuc”, “D”)).
‘noneuc’ has its own link scale (default ‘log’) on which it may be modelled as a linear function of any of the predictors available for density (x, y, x2, y2, xy, session, Session, g, or any mask covariate – see Chapter 11. It may also, in principle, be modelled using regression splines (Borchers & Kidney, 2014), but this is untested. When the model is fitted by secr.fit
, the beta parameters for the ‘noneuc’ sub-model are estimated along with all the others. To make noneuc available to userdist, ensure that it appears in the ‘model’ argument. Use the formula noneuc ~ 1 if noneuc is constant.
The function may compute least-cost paths via intervening mask cells using the powerful igraph package (Csardi & Nepusz, 2006). This is most easily accessed with package gdistance, which in turn uses the RasterLayer S4 object class from the package raster. To facilitate this, secr includes code to treat the ‘mask’ S3 class as a virtual S4 class, and provides a method for the function ‘raster’ to convert a mask to a RasterLayer.
If the function generates any bad distances (negative, infinite or missing) these will be replaced by 1e10, with a warning.
F.4 Examples
We use annotated examples to show how the userdist function may be used to define different models. For illustration we use the Orongorongo Valley brushtail possum dataset from February 1996 (OVpossumCH). The data are captures of possums over 5 nights in single-catch traps at 30-m spacing. We start by extracting the data, defining a habitat mask, and fitting a null model:
code to construct ovposs and ovmask
datadir <- system.file("extdata", package = "secr")
ovforest <- sf::st_read (paste0(datadir, "/OVforest.shp"),
quiet = TRUE)
ovposs <- OVpossumCH[[1]] # select February 1996
ovmask <- make.mask(traps(ovposs), buffer = 120, type = "trapbuffer",
poly = ovforest[1:2,], spacing = 7.5, keep.poly = FALSE)
# for plotting only
leftbank <- read.table(paste0(datadir,"/leftbank.txt"))[21:195,]
fit0 <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN", trace = FALSE)
Warning: multi-catch likelihood used for single-catch traps
The warning is routine: we will suppress it in later examples. The distance functions below are not specific to a particular study: each may be applied to other datasets.
F.4.1 Scale of movement \sigma depends on location of home-range centre
In this simple case we use the non-Euclidean distance function to model continuous spatial variation in \sigma. This cannot be done directly in secr because sigma is treated as part of the detection model, which does not allow for continuous spatial variation in its parameters. Instead we model spatial variation in ‘noneuc’ as a stand-in for ‘sigma’
fn1 <- function (xy1, xy2, mask) {
if (missing(xy1)) return("noneuc")
sig <- covariates(mask)$noneuc # sigma(x,y) at mask points
sig <- matrix(sig, byrow = TRUE, nrow = nrow(xy1), ncol = nrow(xy2))
euc <- edist(xy1, xy2)
euc / sig
}
fit1 <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN",
details = list(userdist = fn1), model = noneuc ~ x + y +
x2 + y2 + xy, fixed = list(sigma = 1), trace = FALSE)
predict(fit1)
link estimate SE.estimate lcl ucl
D log 14.68433 1.0914749 12.696150 16.98385
lambda0 log 0.10852 0.0096263 0.091235 0.12908
noneuc log 25.92426 1.3019339 23.495538 28.60404
We can take the values of noneuc directly from the mask covariates because we know xy2 and mask are the same points. We may sometimes want to use fn1 in context where this does not hold, e.g., when simulating data.
fn1a <- function (xy1, xy2, mask) {
if(missing(xy1)) return("noneuc")
xy1 <- addCovariates(xy1, mask)
sig <- covariates(xy1)$noneuc # sigma(x,y) at detectors
sig <- matrix(sig, nrow = nrow(xy1), ncol = nrow(xy2))
euc <- edist(xy1, xy2)
euc / sig
}
fit1a <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN", trace =
FALSE, details = list(userdist = fn1a), model = noneuc ~
x + y + x2 + y2 + xy, fixed = list(sigma = 1))
predict(fit1a)
link estimate SE.estimate lcl ucl
D log 14.46194 1.0296891 12.580493 16.62476
lambda0 log 0.10758 0.0095253 0.090471 0.12792
noneuc log 26.12539 1.4198769 23.487423 29.05964
We can verify the use of ‘noneuc’ in fn1 by using it to re-fit the null model:
predict(fit0)
link estimate SE.estimate lcl ucl
D log 14.38020 1.0030935 12.544728 16.48423
lambda0 log 0.10148 0.0089476 0.085402 0.12058
sigma log 27.37653 0.9729740 25.535004 29.35087
predict(fit0a)
link estimate SE.estimate lcl ucl
D log 14.38019 1.0030937 12.544720 16.48422
lambda0 log 0.10148 0.0089477 0.085403 0.12058
noneuc log 27.37645 0.9729683 25.534936 29.35078
Here, fitting noneuc as a constant while holding sigma fixed is exactly the same as fitting sigma alone.
F.4.2 Scale of movement \sigma depends on locations of both home-range centre and detector
Hypothetically, detections at xy1 of an animal centred at xy2 may depend on both locations (this may also be seen as a approximation to the following case of continuous variation along the path between xy1 and xy2). To model this we need to retrieve the value of noneuc for both locations. Within fn2 we use addCovariates
to extract the covariates of the mask (and hence noneuc) for each point in xy1 and xy2. The call to secr.fit
is identical except that it uses fn2 instead of fn1:
fn2 <- function (xy1, xy2, mask) {
if (missing(xy1)) return("noneuc")
xy1 <- addCovariates(xy1, mask)
xy2 <- addCovariates(xy2, mask)
sig1 <- as.numeric(covariates(xy1)$noneuc) # sigma at detector
sig2 <- as.numeric(covariates(xy2)$noneuc) # sigma at mask pt
euc <- edist(xy1, xy2)
sig <- outer (sig1, sig2, FUN = function(s1, s2) (s1 + s2)/2)
euc / sig
}
fit2 <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN",
details = list(userdist = fn2), model = noneuc ~ x + y +
x2 + y2 + xy, fixed = list(sigma = 1), trace = FALSE)
predict(fit2)
link estimate SE.estimate lcl ucl
D log 14.54654 1.0580294 12.616245 16.77218
lambda0 log 0.10781 0.0095493 0.090662 0.12821
noneuc log 26.02329 1.3510114 23.507232 28.80866
The value of noneuc reported by predict.secr
is the predicted value at the centroid of the mask, because the model uses standardised mask coordinates.
F.4.3 Continuously varying \sigma using gdistance
A more elegant but slower approach is to find the least-cost path across the network of cells between xy1 and xy2, using noneuc (i.e. sigma ) as the cell-specific cost weighting (large cell-specific sigma equates with greater ‘conductance’, the inverse of friction or cost). For this we use functions from the package gdistance, which in turn uses igraph.
fn3 <- function (xy1, xy2, mask) {
if (missing(xy1)) return("noneuc")
# warp distances to be \propto \int_along path sigma(x,y) dp
# where p is path distance
if (!require(gdistance))
stop ("install package gdistance to use this function")
# make raster from mask
Sraster <- raster(mask, "noneuc")
# Assume animals can traverse gaps: bridge gaps using mean
Sraster[is.na(Sraster[])] <- mean(Sraster[], na.rm = TRUE)
# TransitionLayer
tr <- transition(Sraster, transitionFunction = mean,
directions = 16)
tr <- geoCorrection(tr, type = "c", multpl = FALSE)
# costDistance
costDistance(tr, as.matrix(xy1), as.matrix(xy2))
}
fit3 <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN",
details = list(userdist = fn3), model = noneuc ~ x + y +
x2 + y2 + xy, fixed = list(sigma = 1), trace = FALSE)
predict(fit3)
link estimate SE.estimate lcl ucl
D log 14.4063 1.0500177 12.490935 16.6154
lambda0 log 0.1076 0.0095041 0.090529 0.1279
noneuc log 26.4338 1.3645616 23.891813 29.2463
The gdistance function costDistance
uses a TransitionLayer object that essentially describes the connections between cells in a RasterLayer. In transition
adjacent cells are assigned a positive value for ‘conductance’ and all other cells a zero value. Adjacency is defined by the directions argument as 4 (rook’s case), 8 (queen’s case), 16 (knight and one-cell queen moves) and possibly other values (see ?adjacent in gdistance). Values < 16 can considerably distort distances even if conductance is homogeneous. geoCorrection
is needed to allow for the greater separation (\times \sqrt 2) of cell centres measured along diagonals.
In ovmask
there are two forest blocks separated by a shingle stream bed and low scrub that is easily crossed by possums but does not count as ‘habitat’. Habitat gaps are assumed in secr to be traversible. The opposite is assumed by gdistance. To coerce gdistance to behave like secr we here temporarily fill in the gaps.
The argument ‘transitionFunction’ determines how the conductance values of adjacent cells are combined to weight travel between them. Here we simply average them, but any other single-valued function of 2 inputs can be used.
Integrating along the path (fn3) takes about 3.6 times as long as the approximation (fn2) and gives quite similar results.
F.4.4 Density-dependent \sigma
A more interesting variation makes sigma a function of the cell-specific density, which may vary independently across space (Efford et al., 2016). Specifically, \sigma(x,y) = k / \sqrt{D(x,y)}, where k is the fitted parameter (noneuc).
fn4 <- function (xy1, xy2, mask) {
if(missing(xy1)) return(c("D", "noneuc"))
if (!require(gdistance))
stop ("install package gdistance to use this function")
# make raster from mask
D <- covariates(mask)$D
k <- covariates(mask)$noneuc
Sraster <- raster(mask, values = k / D^0.5)
# Assume animals can traverse gaps: bridge gaps using mean
Sraster[is.na(Sraster[])] <- mean(Sraster[], na.rm = TRUE)
# TransitionLayer
tr <- transition(Sraster, transitionFunction = mean,
directions = 16)
tr <- geoCorrection(tr, type = "c", multpl = FALSE)
# costDistance
costDistance(tr, as.matrix(xy1), as.matrix(xy2))
}
fit4 <- secr.fit(ovposs, mask = ovmask, detectfn = "HHN", trace =
FALSE, details = list(userdist = fn4), fixed = list(sigma = 1),
model = list(noneuc ~ 1, D ~ x + y + x2 + y2 + xy))
predict(fit4)
link estimate SE.estimate lcl ucl
D log 15.45490 1.6387186 12.562138 19.01380
lambda0 log 0.10669 0.0094078 0.089788 0.12678
noneuc log 103.22821 5.0334083 93.824969 113.57385
predict(fit4a)
link estimate SE.estimate lcl ucl
D log 15.76414 1.7672732 12.663161 19.62448
lambda0 log 0.10681 0.0094166 0.089887 0.12691
noneuc log 103.08276 5.0090010 93.723571 113.37656
F.4.5 Habitat model for connectivity
Yet another possibility, in the spirit of Royle et al. (2013), is to model conductance as a function of habitat covariates. As usual in secr these are stored as one or more mask covariates. It is easy to add a covariate for forest type (Nothofagus-dominant ‘beech’ vs ‘nonbeech’) to our mask:
predict(fit5, newdata =
data.frame(forest = c("beech", "nonbeech")))
$`forest = beech`
link estimate SE.estimate lcl ucl
D log 9.42514 2.6531406 5.48569 16.19364
lambda0 log 0.10111 0.0089337 0.08506 0.12019
noneuc log 29.55771 3.4060705 23.59969 37.01991
$`forest = nonbeech`
link estimate SE.estimate lcl ucl
D log 15.67451 1.2679355 13.37985 18.36271
lambda0 log 0.10111 0.0089337 0.08506 0.12019
noneuc log 27.23476 1.0319163 25.28619 29.33349
Note that we have re-used the userdist function fn2, and allowed both density and noneuc (sigma) to vary by forest type. Strictly, we should have identified “forest” as a required covariate in the (re)definition of fn2, but this is obviously not critical.
A full analysis should also consider models with variation in lambda0. There is no simple way in secr to model continuous spatial variation in lambda0 as a function of home-range location (cf sigma in Example 1 above). However, variation in lambda0 at the point of detection may be modelled with detector-level covariates (Section 10.2.1).
F.5 And the winner is…
Now that we have a bunch of fitted models, let’s see which does the best:
model npar AIC dAIC AICwt
fit4a D~s(x, y, k = 6) lambda0~1 noneuc~1 8 3098.1 0.000 0.4549
fit4 D~x + y + x2 + y2 + xy lambda0~1 noneuc~1 8 3098.5 0.382 0.3758
fit1 D~1 lambda0~1 noneuc~x + y + x2 + y2 + xy 8 3101.8 3.704 0.0714
fit3 D~1 lambda0~1 noneuc~x + y + x2 + y2 + xy 8 3102.4 4.304 0.0529
fit2 D~1 lambda0~1 noneuc~x + y + x2 + y2 + xy 8 3103.5 5.411 0.0304
fit1a D~1 lambda0~1 noneuc~x + y + x2 + y2 + xy 8 3105.0 6.862 0.0147
fit0 D~1 lambda0~1 sigma~1 3 3118.1 19.979 0.0000
fit0a D~1 lambda0~1 noneuc~1 3 3118.1 19.979 0.0000
fit5 D~forest lambda0~1 noneuc~forest 5 3118.4 20.280 0.0000
…the model with a quadratic or spline trend in density and density-dependent sigma.
F.6 Notes
The ‘real’ parameter for spatial scale (\sigma) is lurking in the background as part of the detection model. User-defined non-Euclidean distances are used in the detection function just like ordinary Euclidean distances. This means in practice that they are (almost) always divided by \sigma. Formally: the distance d_{ij} between an animal i and a detector j appears in all commonly used detection functions as the ratio r_{ij} = d_{ij}/\sigma (e.g., halfnormal \lambda = \lambda_0 \exp(-0.5r_{ij}^2) and exponential \lambda = \lambda_0 \exp(-r_{ij})).
What if we want non-Euclidean distances, but do not want to estimate noneuc? This is a perfectly reasonable request if sigma is constant across space and the distance computation is determined entirely by the habitat geometry, with no need for an additional parameter. If ‘noneuc’ is not included in the character vector returned by your userdist function when it is called with no arguments then noneuc is not modelled at all. This is the default in secrlinear.
Providing a suitable initial value for ‘noneuc’ can be a problem. The argument ‘start’ of secr.fit
may be a named, and possibly incomplete, list of real parameter values, so a call such as this is valid:
secr.fit (captdata, model = noneuc~1, details = list(userdist = fn2),
trace = FALSE, start = list(noneuc = 25), fixed = list(sigma = 1))
secr.fit(capthist = captdata, model = noneuc ~ 1, start = list(noneuc = 25),
fixed = list(sigma = 1), details = list(userdist = fn2),
trace = FALSE)
secr 5.2.0, 20:24:43 13 Feb 2025
Detector type single
Detector number 100
Average spacing 30 m
x-range 365 635 m
y-range 365 635 m
N animals : 76
N detections : 235
N occasions : 5
Mask area : 21.227 ha
Model : D~1 g0~1 noneuc~1
User distances : dynamic (function)
Fixed (real) : sigma = 1
Detection fn : halfnormal
Distribution : poisson
N parameters : 3
Log likelihood : -759.03
AIC : 1524.1
AICc : 1524.4
Beta parameters (coefficients)
beta SE.beta lcl ucl
D 1.70107 0.117615 1.4705 1.93159
g0 -0.97849 0.136239 -1.2455 -0.71147
noneuc 3.37983 0.044415 3.2928 3.46688
Variance-covariance matrix of beta parameters
D g0 noneuc
D 0.01383322 0.00015578 -0.00099075
g0 0.00015578 0.01856108 -0.00334338
noneuc -0.00099075 -0.00334338 0.00197273
Fitted (real) parameters evaluated at base levels of covariates
link estimate SE.estimate lcl ucl
D log 5.47980 0.646740 4.35162 6.90047
g0 logit 0.27319 0.027051 0.22348 0.32927
noneuc log 29.36583 1.304938 26.91757 32.03677
We have ignored the parameter \lambda_0. This is almost certainly a mistake, as large variation in \sigma without compensatory or normalising variation in \lambda_0 is biologically implausible and can lead to improbable results Efford (2014).
It is intended that non-Euclidean distances should work with all relevant functions in secr.
You may be tempted to model ‘noneuc’ as a function of group - after all, D~g is permitted, right? Unfortunately, this will not work. There is only one pre-computed distance matrix, not one matrix per group.
F.7 Simulation after Sutherland et al. (2015)
Sutherland et al. (2015) simulated SECR data from a population of animals whose movement was channeled to varying extents along a dendritic network (river system). Their model treated the habitat as 2-dimensional and shrank distances for pixels close to water and expanded them for pixels further away. The authors kindly provided data for the network map and detector layout which we use here to emulate their simulations in secr. We assume an existing SpatialLinesDataFrame sample.water
for the network, and a matrix of x-y coordinates for detector locations gridTrapsXY
. rivers
is a version of sample.water
clipped to the habitat mask and used only for plotting.
# use package secrlinear to create a discretised version of the
# network as a handy way to get distance to water
# loading secrlinear also loads secr
library(secrlinear)
library(gdistance)
swlinearmask <- read.linearmask(data = sample.water, spacing = 100)
# generate secr traps object from detector locations
tr <- data.frame(gridTrapsXY*1000) # convert to metres
names(tr) <- c("x","y")
tr <- read.traps(data=tr, detector = "count")
# generate 2-D habitat mask
sw2Dmask <- make.mask(tr, buffer = 3950, spacing = 100)
d2w <- distancetotrap(sw2Dmask, swlinearmask)
covariates(sw2Dmask) <- data.frame(d2w = d2w/1000) # km to water
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
The distance function requires a value of the friction parameter ‘noneuc’ for each mask pixel. Distances are approximated using gdistance functions as before, except that we interpret the distance-to-water scale as ‘friction’ and invert that for gdistance.
dfn <- function (xy1, xy2, mask) {
if (missing(xy1)) return("noneuc")
require(gdistance)
Sraster <- raster(mask, "noneuc")
# conductance is inverse of friction
trans <- transition(Sraster, transitionFunction =
function(x) 1/mean(x), directions = 16)
trans <- geoCorrection(trans)
costDistance(trans, as.matrix(xy1), as.matrix(xy2))
}
The Royle et al. (2013) and Sutherland et al. (2015) models use an (\alpha_0, \alpha_1) parameterisation instead of (\lambda_0, \sigma). Their \alpha_2 translates directly to a coefficient in the secr model, as we’ll see. We consider just one realisation of one scenario (the package secrdesign Efford (2023) manages replicated simulations of multiple scenarios).
# parameter values from Sutherland et al. 2014
alpha0 <- -1 # implies lambda0 = invlogit(-1) = 0.2689414
sigma <- 1400
alpha1 <- 1 / (2 * sigma^2)
alpha2 <- 5 # just one scenario from the range 0..10
K <- 10 # sampling over 10 occasions, collapsed to 1 occ
Now we are ready to build a simulated dataset.
# simulate fixed population of 200 animals in masked area
pop <- sim.popn (D = 200/nrow(sw2Dmask), core = tr,
buffer = 3950, Ndist = "fixed")
# to simulate non-Euclidean detection we attach a mask with
# the pixel-specific friction to the simulated popn object
covariates(sw2Dmask)$noneuc <- exp(alpha2 *
covariates(sw2Dmask)$d2w)
attr(pop, "mask") <- sw2Dmask
# simulate detections, specifying non-Euclidean distance function
CH <- sim.capthist(tr, pop = pop, userdist = dfn,
noccasions = 1, binomN = K, detectpar = list(lambda0 =
invlogit(alpha0), sigma = sigma), detectfn = "HHN")
summary(CH, moves = TRUE)
Object class capthist
Detector type count
Detector number 64
Average spacing 1385.7 m
x-range 1698699 1708399 m
y-range 2387891 2398391 m
Counts by occasion
1 Total
n 37 37
u 37 37
f 37 37
M(t+1) 37 37
losses 0 0
detections 109 109
detectors visited 33 33
detectors used 64 64
Number of movements per animal
0 1 2 3
16 13 7 1
Distance moved, excluding zero (m)
Min. 1st Qu. Median Mean 3rd Qu. Max.
1386 1386 1443 1552 1500 3151
Individual covariates
sex
F:21
M:16
Model fitting is simple, but the default starting value for noneuc is not suitable and is overridden:
The warning from nlm indicates a potential problem, but the standard errors and confidence limits below look plausible (they could be checked by running secr.refit
with method = “none”). Fitting is slow (4 minutes on an aging PC). This is partly because the mask is large (32384 pixels) in order to maintain resolution in relation to the stream network.
coef(fitne1)
beta SE.beta lcl ucl
D -5.3042 0.181714 -5.6603 -4.94800
lambda0 -1.1386 0.169830 -1.4715 -0.80577
sigma 7.1491 0.091395 6.9699 7.32821
noneuc.d2w 4.5961 0.503034 3.6102 5.58205
predict(fitne1)
link estimate SE.estimate lcl ucl
D log 4.9709e-03 9.1079e-04 3.4814e-03 7.0976e-03
lambda0 log 3.2026e-01 5.4784e-02 2.2958e-01 4.4674e-01
sigma log 1.2729e+03 1.1658e+02 1.0642e+03 1.5226e+03
noneuc log 7.5395e+00 1.6876e+00 4.8881e+00 1.1629e+01
region.N(fitne1)
estimate SE.estimate lcl ucl n
E.N 160.98 29.495 112.74 229.85 37
R.N 160.98 26.627 118.77 224.97 37
The coefficient noneuc.d2w corresponds to alpha2. Estimates of predicted (‘real’) parameters D and lambda0, and the coefficient noneuc.d2w, and are comfortably close to the true values, and all true values are covered by the 95% CI.
We fit the ‘noneuc’ (friction) parameter through the origin (zero intercept; -1 in formula). The predicted value of ‘noneuc’ relates to the covariate value for the first pixel in the mask (d2w = 1.133 km), but in this zero-intercept model the meaning of ‘noneuc’ itself is obscure. In effect, the parameter alpha1 (or sigma) serves as the intercept; the same model may be fitted by fixing sigma (fixed = list(sigma = 1)
) and estimating an intercept for noneuc (model = noneuc ~ d2w
). In this case, ‘noneuc’ may be interpreted as the site-specific sigma (see also examples in the main text).
It is interesting to plot the predicted detection probability under the simulated model. For plotting we add the pdot value as an extra covariate of the mask. Note that pdot here uses the ‘noneuc’ value previously added as a covariate to sw2Dmask
.
covariates(sw2Dmask)$predicted.pdot <- pdot(sw2Dmask, tr,
noccasions = 1, binomN = 10, detectfn = "HHN", detectpar =
list(lambda0 = invlogit(-1), sigma = sigma), userdist = dfn)
Mixing 2-km and 0.5-km cells carries a slight penalty: the centres of a few 2-km cells (<1%) do not lie in valid 0.5-km cells; these become inaccessible (infinite distance from all detectors) and are silently dropped in a later step.↩︎