#' Angle Analysis
#'
#' Analyzing angles to reference directions, points, or planes can be useful to detect
#' artefacts and/or directionality in tracking datasets (Beltman et al, 2009). All these
#' functions take a track and a reference (point/direction/plane)
#' as input and return a distance or angle as output. Angles/distances are by default
#' computed to the first step in the given track.
#'
#'
#' @details
#'
#' \code{\link{angleToPoint}} and \code{\link{distanceToPoint}} return the angle/distance of the track to the
#' reference point. The distance returned is between the first coordinate in the track and the
#' reference point. The angle is between the overall displacement vector of the track and the
#' vector from its first coordinate to the reference point. Angles are by default returned in
#' degrees, use \code{degrees=FALSE} to obtain radians. These functions are useful to detect
#' directional bias towards a point of interest, which would result in an average angle of less
#' than 90 degrees with the reference point (especially for tracks at a small distance to the
#' reference point).
#'
#' \code{\link{angleToPlane}} and \code{\link{distanceToPlane}} return the angle/distance of the track to a
#' plane of interest. This plane must be specified by three points lying on it.
#' The distance returned is between the first coordinate in the track and the
#' reference point. The angle is between the overall displacement vector of the track and the
#' plane of interest. These functions are useful to detect tracking artefacts near the borders
#' of the imaging volume. Use \code{\link{boundingBox}} to guess where those borders are.
#' Angles are by default returned in
#' degrees, use \code{degrees=FALSE} to obtain radians.
#'
#' \code{\link{angleToDir}} returns the angle of a track's overall displacement vector
#' to a direction of interest.
#' This function is useful to detect directionality in cases where the direction of the bias is
#' known in advance (e.g. when cells are known to move up a chemotactic gradient): in that case,
#' the average angle to the reference direction should be less than 90 degrees. Angles are
#' by default returned in degrees, use \code{degrees=FALSE} to obtain radians.
#'
#' \code{\link{angleSteps}} and \code{\link{distanceSteps}} return the angle/distance between a pair
#' of steps in the data that occur at the same timepoint. Angles are in degrees by default,
#' use \code{degrees=FALSE} to obtain radians. Use \code{\link{stepPairs}} to extract all pairs of
#' steps that occur at the same timepoint, and use \code{\link{analyzeStepPairs}} to do this and then
#' also obtain the angles and distances for each of these pairs.
#'
#' \code{\link{angleCells}} and \code{\link{distanceCells}} return the angle/distance between a pair
#' of tracks in the data. The computed angles are between the overall displacement vectors of the
#' tracks, the distance is the shortest distance between them at any timepoint they share.
#' Angles are in degrees by default, use \code{degrees=FALSE} to obtain radians.
#' Use \code{\link{cellPairs}} to extract all pairs of
#' cells in the data, and use \code{\link{analyzeCellPairs}} to do this and then
#' also obtain the angles and distances for each of these pairs.
#'
#' @name AngleAnalysis
#'
#' @seealso \code{\link{TrackMeasures}} for other measures that can be used to quantify tracks.
#'
#' See the vignettes on Quality Control and Track Analysis for more detailed examples of
#' angle analyses.
#' \code{browseVignettes( package = "celltrackR" )}
#'
#' @return This page is for documentation only and provides an overview of angle analysis functions
#' and their use cases. The return values of each of these functions are documented separately;
#' please follow the link to the documentation page of that specific function.
#'
#' @examples
#' ## Plotting the angle versus the distance to a reference point can be informative to
#' ## detect biased movement towards that point. We should be suspicious especially
#' ## when small angles are more frequent at lower distances.
#' steps <- subtracks( sample( Neutrophils, 50 ), 1 )
#' bb <- boundingBox( Neutrophils )
#' angles <- sapply( steps, angleToPoint, p = bb["max",-1] )
#' distances <- sapply( steps, distanceToPoint, p = bb["max",-1] )
#' scatter.smooth( distances, angles )
#' abline( h = 90, col = "red" )
#'
#' ## Get a distribution of Neutrophil step angles with the reference direction
#' ## in positive y direction. The histogram is enriched for low angles, suggesting
#' ## directed movement:
#' hist( sapply( steps, angleToDir, dvec=c(1,-1) ) )
#'
#' ## Plotting the angle versus the distance to a reference plane can be informative to
#' ## detect tracking artefacts near the border of the imaging volume.
#' ## We should be suspicious especially when small angles are more frequent at low distances
#' ## to the border planes; as is the case in the z-dimension for the raw data:
#' load( system.file("extdata", "TCellsRaw.rda", package="celltrackR" ) )
#' steps <- subtracks( sample( TCellsRaw, 50 ), 1 )
#' minz <- boundingBox( TCellsRaw )["min","z"]
#' ## Compute angles and distances to the lower plane in z-dimension
#' angles <- sapply( steps, angleToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' distances <- sapply( steps, distanceToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' scatter.smooth( distances, angles )
#' abline( h = 32.7, col = "red" )
#'
#' ## Plot distance versus angle for all cell pairs (here in only a sample to speed things up)
#' pairs <- analyzeCellPairs( sample( TCells, 50 ) )
#' scatter.smooth( pairs$dist, pairs$angle )
#' abline( h = 90, col = "red" )
#'
#' ## Plot distance versus angle for all step pairs, filtering for those that
#' ## displace at least 2 microns
#' pairs <- analyzeStepPairs( sample( TCells, 50 ), filter.steps = function(t) displacement(t) > 2 )
#' scatter.smooth( pairs$dist, pairs$angle )
#' abline( h = 90, col = "red" )
#'
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
NULL
#' Angle Between Two Vectors
#'
#' Compute the angle between two vectors a and b, which can be numeric vectors
#' or matrices in which each row represents a numeric vector.
#' In the last case, one angle is returned for each row. By default, angles
#' are returned in degrees -- set \code{degrees = TRUE} to return radians.
#'
#' @param a the first vector or set of vectors. Must be a numeric vector or a
#' matrix where each row represents a numeric vector.
#' @param b the second vector or set of vectors, for which angles with the
#' vector (set) a must be computed. Must have the same dimensions as a.
#' @param degrees logical: if \code{TRUE} (default), return angles in degrees instead of radians.
#'
#' @return A single angle (if a and b are single vectors) or a numeric vector of
#' angles (if a and b are matrices; in that case, the output vector contains one
#' angle for each row in matrices a and b).
#'
#' @examples
#' ## The angle between the vectors [0,1] and [1,0] is 90 degrees:
#' vecAngle( c(0,1), c(1,0) )
#' ## The same holds for 3D angles:
#' vecAngle( c(0,1,0), c(1,0,0) )
#' @export
vecAngle <- function( a, b, degrees = TRUE )
{
# Check if inputs have the right dimensions
if( !( is.numeric(a) && is.numeric(b) && (is.matrix(a) == is.matrix(b)) ) ){
stop( "vecAngle: a and b must both be numeric vectors of the same length or matrices of the same size")
}
if( any( pracma::size(a) != pracma::size(b) ) ){
stop( "vecAngle: cannot compute angle between vectors of unequal dimensions.")
}
# Convert vector to matrix if necessary
if( !is.matrix(a) ){
a <- matrix( a, ncol = length(a) )
b <- matrix( b, ncol = length(b) )
}
# Normalize a and b by their length
a <- a/sqrt(.rowSums(a^2, nrow(a), ncol(a)))
b <- b/sqrt(.rowSums(b^2, nrow(b), ncol(b)))
# Compute dot product
rs <- .rowSums(a * b, nrow(a), ncol(a))
rs[rs > 1] <- 1
rs[rs < -1] <- -1
# angle is the acos of the dot product of the normalized vectors
ang <- acos(rs)
if( degrees ){
return( pracma::rad2deg(ang) )
} else{
return(ang)
}
}
#' Angle with a Reference Point
#'
#' Compute the angle between a track's overall displacement vector and the vector from
#' it's first coordinate to a reference point. Useful to
#' detect directed movement towards a point (see examples).
#'
#' @param x a single input track; a matrix whose first column is time and whose
#' remaining columns are a spatial coordinate.
#' @param p numeric vector of coordinates of the reference point p to compute angles/distances to.
#' @param from index, or vector of indices, of the first row of the track. If
#' \code{from} is a vector, angles are returned for all steps starting at
#' the indices in \code{from}.
#' @param degrees logical; should angles be returned in degrees rather than radians? (default = TRUE).
#'
#'
#' @return A single angle.
#'
#' @details The average angle of steps to a reference point should be 90 degrees if there is
#' no bias towards movement in the direction of the reference point. If there is such a bias,
#' there should be an enrichment of smaller angles. The expected distribution without bias
#' is a uniform distribution in 2D or a sine distribution in 3D (Beltman et al, 2009).
#'
#' @seealso \code{\link{distanceToPoint}} to compute the distance to the reference point, and
#' \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
#'
#' @examples
#' ## Get a distribution of step angles with a reference point
#' ## Use bb to get the corner with highest x,y (,z) value
#' ## The histogram is enriched for low angles, suggesting directed movement:
#' steps <- subtracks( Neutrophils, 1 )
#' bb <- boundingBox( Neutrophils )
#' hist( sapply( steps, angleToPoint, p = bb["max",-1] ) )
#'
#' ## The same does not hold for movement of T cells towards the point (0,0)
#' steps <- subtracks( TCells, 1 )
#' hist( sapply( steps, angleToPoint, p = c(0,0) ) )
#'
#' ## Plotting the angle versus the distance to the reference point can also be informative,
#' ## especially when small angles are more frequent at lower distances.
#' angles <- sapply( steps, angleToPoint, p = bb["max",-1] )
#' distances <- sapply( steps, distanceToPoint, p = bb["max",-1] )
#' scatter.smooth( distances, angles )
#' abline( h = 90, col = "red" )
#' @export
angleToPoint <- function (x, p = c(1,1,1), from = 1, degrees = TRUE )
{
# Check if the given point has the correct dimensions
if( length(p) != ncol(x) - 1 ){
stop("In angleToPoint: Reference point coordinates must have the same number of dimensions as
coordinates in the tracking data.")
}
# If point is equal to the first point in the track, the angle is undefined.
if( all( p == x[1,-1] ) ){
warning("In angleToPoint: Reference point is equal to the track starting point. Angle
is undefined, returning NA.")
return(NA)
}
# Initialize a vector of NAs for every index in "from".
# This will be replaced if possible.
r <- rep(NA, length(from))
# To compute an angle we need at least one step (two timepoints).
ft <- from < nrow(x)
if( sum(ft) > 0 ){
# x,y,z components of the step vectors a to compute angle with refpoint with
starts <- x[from[ft], -1, drop = FALSE]
# displacements from these 'from' points to the track endpoint
a <- t( apply( starts, 1, function(t) x[nrow(x),-1] - t ) )
# compute vectors of step starting points with the refpoint
b <- x[from[ft],-1,drop=FALSE]
b <- t( apply( b, 1, function(x) p - x ) )
# compute angle
r[ft] <- vecAngle( a, b, degrees = degrees )
}
r
}
#' Angle with a Reference Direction
#'
#' Compute the angle between a track's overall displacement and a reference direction.
#' Useful to detect biased movement when the directional bias is known (see examples).
#'
#' @param x a single input track; a matrix whose first column is time and whose
#' remaining columns are a spatial coordinate.
#' @param dvec numeric vector specifying a reference direction to compute angles to.
#' @param from index, or vector of indices, of the first row of the track. If
#' \code{from} is a vector, angles are returned for all steps starting at
#' the indices in \code{from}.
#' @param degrees logical; should angles be returned in degrees rather than radians? (default = TRUE).
#'
#' @return A single angle.
#'
#' @details The average angle of steps to a reference direction should be 90 degrees if there is
#' no bias towards movement in the direction of the reference point. If there is such a bias,
#' there should be an enrichment of smaller angles. The expected distribution without bias
#' is a uniform distribution in 2D or a sine distribution in 3D (Beltman et al, 2009).
#'
#' @seealso \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
#'
#' @examples
#' ## Get a distribution of Neutrophil step angles with the reference direction in positive
#' ## y direction. The histogram is enriched for low angles, suggesting directed movement:
#' steps <- subtracks( Neutrophils, 1 )
#' hist( sapply( steps, angleToDir, dvec=c(1,-1) ) )
#' @export
angleToDir <- function (x, dvec = c(1,1,1), from = 1, degrees=TRUE )
{
# Check if the given direction has the correct dimensions
if( length(dvec) != ncol(x) - 1 ){
stop("In angleToDir: Direction vector must have the same number of dimensions as
coordinates in the tracking data.")
}
# Initialize a vector of NAs for every index in "from".
# This will be replaced if possible.
r <- rep(NA, length(from))
# To compute an angle we need at least one step (two timepoints).
ft <- from < nrow(x)
if( sum(ft) > 0 ){
# x,y,z components of the step vectors a to compute angle with dvec with
starts <- x[from[ft], -1, drop = FALSE]
# displacements from these 'from' points to the track endpoint
a <- t( apply( starts, 1, function(t) x[nrow(x),-1] - t ) )
b <- matrix( rep( dvec, nrow(a)),
ncol = length(dvec), byrow = TRUE )
# Normalize step vectors and the ref direction vector by their length
#a <- a/sqrt(.rowSums(a^2, nrow(a),ncol(a)))
#b <- dvec/sqrt( sum( dvec^2 ) )
# Dot product of normalized a and b; this equals cos alpha
#rs <- .rowSums(a * b, nrow(a), ncol(a))
#rs[rs > 1] <- 1
#rs[rs < -1] <- -1
# angle is acos.
r[ft] <- vecAngle( a, b, degrees = degrees )
}
r
}
#' Angle with a Reference Plane
#'
#' Compute the angle between a track's overall displacement and a reference plane.
#' Useful to detect directed movement and/or tracking artefacts.
#'
#' @param x a single input track; a matrix whose first column is time and whose
#' remaining columns are a spatial coordinate.
#' @param p1,p2,p3 numeric vectors of coordinates of three points specifying a reference plane to
#' compute distances to.
#' @param from index, or vector of indices, of the first row of the track. If
#' \code{from} is a vector, angles are returned for all steps starting at
#' the indices in \code{from}.
#' @param degrees logical; should angles be returned in degrees rather than radians? (default = TRUE).
#'
#' @return A single angle.
#'
#' @details The average angle of steps to a reference plane should be roughly 32.7 degrees.
#' Lower angles to the border planes of an imaging volume can be indicative of tracking
#' artefacts, and systematic deviations from 32.7 can indicate a directional bias
#' (Beltman et al, 2009).
#'
#' @seealso \code{\link{distanceToPlane}} to compute the distance to the reference plane, and
#' \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
#'
#' @examples
#' ## Plotting the angle versus the distance to a reference plane can be informative to
#' ## detect tracking artefacts near the border of the imaging volume.
#' ## We should be suspicious especially when small angles are more frequent at low distances
#' ## to the border planes.
#' load( system.file("extdata", "TCellsRaw.rda", package="celltrackR" ) )
#' steps <- subtracks( TCellsRaw, 1 )
#' minz <- boundingBox( TCellsRaw )["min","z"]
#' ## Compute angles and distances to the lower plane in z-dimension
#' angles <- sapply( steps, angleToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' distances <- sapply( steps, distanceToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' scatter.smooth( distances, angles )
#' abline( h = 32.7, col = "red" )
#' @export
angleToPlane <- function (x, p1 = c(0,0,0), p2 = c(0,1,0), p3 = c(1,0,0),
from = 1, degrees =TRUE )
{
# Check if the given points have the correct dimensions
plengths <- c( length(p1), length(p2), length(p3) )
if( length(unique(plengths)) > 1 ){
stop("In angleToPlane: Points p1,p2,p3 specifying the plane must have the same number of coordinates.")
}
if( unique(plengths) != 3 || ncol(x)-1 != 3 ){
stop("In angleToPlane: Method is only supported for three-dimensional data.")
}
pmatrix <- matrix( c(p1,p2,p3), nrow = 3, byrow = TRUE )
if( any( duplicated( pmatrix ) ) ){
stop("In angleToPlane: Points p1, p2, and p3 must be three unique points!")
}
if( length(p1) != ( ncol(x) - 1 ) ){
stop("In angleToPlane: Plane points must have the same number of coordinates as
coordinates in the tracking data.")
}
# Check if the given points actually span a plane and are not on the same line.
vec1 <- p2 - p1
vec2 <- p3 - p1
if( vecAngle( vec1, vec2 ) == 0 || vecAngle( vec1, vec2 ) == 180 ){
stop("In angleToPlane: Points p1, p2, and p3 are on the same line and do not fully specify a plane.")
}
# Initialize a vector of NAs for every index in "from".
# This will be replaced if possible.
r <- rep(NA, length(from))
# To compute an angle we need at least one step (two timepoints).
ft <- from < nrow(x)
if( sum(ft) > 0 ){
# x,y,z components of the step vectors a to compute angle with plane with
starts <- x[from[ft], -1, drop = FALSE]
# displacements from these 'from' points to the track endpoint
a <- t( apply( starts, 1, function(t) x[nrow(x),-1] - t ) )
# Compute the normal vector of the plane. Use the three points to get
# two vectors in the plane, then use their cross product to get the normal.
planematrix <- matrix( c(p1,p2,p3), ncol = length(p1), byrow=TRUE )
planevecs <- diff(planematrix)
pnorm <- pracma::crossn( planevecs )
pnorm <- matrix( rep( pnorm, nrow(a)),
ncol = length(pnorm), byrow = TRUE )
# # Normalize step vectors and the plane normal vector by their length
# #a <- a/sqrt(.rowSums(a^2, nrow(a),ncol(a)))
# b <- pnorm/sqrt( sum( pnorm^2 ) )
#
# # Dot product of normalized a and b; this equals cos alpha
# rs <- .rowSums(a * b, nrow(a), ncol(a))
# rs[rs > 1] <- 1
# rs[rs < -1] <- -1
# angle to the plane normal vector
rtmp <- vecAngle( a, pnorm, degrees = degrees )
# angle to the plane itself is 90-r (or pi/2 - r )
deg90 <- ifelse( degrees, 90, pi/2 )
rnew <- abs( deg90 - rtmp )
r[ft] <- rnew
}
return(r)
}
#' Distance to a Reference Plane
#'
#' Compute the (shortest) distance between the starting point of a track and a reference plane.
#' Useful to detect directed movement and/or tracking artefacts.
#'
#' @param x a single input track; a matrix whose first column is time and whose
#' remaining columns are a spatial coordinate.
#' @param p1,p2,p3 numeric vectors of coordinates of three points specifying a reference plane to
#' compute distances to.
#' @param from index, or vector of indices, of the first row of the track. If
#' \code{from} is a vector, distances are returned for all steps starting at
#' the indices in \code{from}.
#'
#' @return A single distance.
#'
#' @seealso \code{\link{angleToPlane}} to compute the angle to the plane, and
#' \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Plotting the angle versus the distance to a reference plane can be informative to
#' ## detect tracking artefacts near the border of the imaging volume.
#' ## We should be suspicious especially when small angles are more frequent at low distances
#' ## to the border planes.
#' load( system.file("extdata", "TCellsRaw.rda", package="celltrackR" ) )
#' steps <- subtracks( TCellsRaw, 1 )
#' minz <- boundingBox( TCellsRaw )["min","z"]
#' ## Compute angles and distances to the lower plane in z-dimension
#' angles <- sapply( steps, angleToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' distances <- sapply( steps, distanceToPlane, p1 = c(0,0,minz), p2 = c(1,0,minz), p3 = c(0,1,minz) )
#' scatter.smooth( distances, angles )
#' abline( h = 32.7, col = "red" )
#' @export
distanceToPlane <- function (x, p1 = c(0,0,0), p2 = c(0,1,0), p3 = c(1,0,0), from = 1 )
{
# Check if the given points have the correct dimensions
plengths <- c( length(p1), length(p2), length(p3) )
if( length(unique(plengths)) != 1){
stop("In distanceToPlane: Points p1,p2,p3 specifying the plane must have the same number of coordinates.")
}
if( unique(plengths) != 3 || ncol(x)-1 != 3 ){
stop("In distanceToPlane: Method is only supported for three-dimensional data.")
}
pmatrix <- matrix( c(p1,p2,p3), nrow = 3, byrow = TRUE )
if( any( duplicated( pmatrix ) ) ){
stop("In distanceToPlane: Points p1, p2, and p3 must be three unique points!")
}
if( length(p1) != ( ncol(x) - 1 ) ){
stop("In distanceToPlane: Plane points must have the same number of coordinates as
coordinates in the tracking data. Currently, the method only supports 3D data.")
}
# Check if the given points actually span a plane and are not on the same line.
vec1 <- p2 - p1
vec2 <- p3 - p1
if( vecAngle( vec1, vec2 ) == 0 || vecAngle( vec1, vec2 ) == 180 ){
stop("In distanceToPlane: Points p1, p2, and p3 are on the same line and do not fully specify a plane.")
}
# Initialize a vector of NAs for every index in "from".
# This will be replaced if possible.
r <- rep(NA, length(from))
# To compute a step distance we need at least one step (two timepoints).
ft <- from < nrow(x)
if( sum(ft) > 0 ){
# x,y,z components of the step startpoints a to compute distance to plane for
a <- x[from[ft], -1, drop = FALSE]
# Compute the normal vector of the plane. Use the three points to get
# two vectors in the plane, then use their cross product to get the normal.
planematrix <- matrix( c(p1,p2,p3), ncol = length(p1), byrow=TRUE )
planevecs <- diff(planematrix)
pnorm <- pracma::crossn( planevecs )
pnorm.len <- sqrt( sum( pnorm^2 ) )
# Compute distance:
D <- -sum( pnorm * p1 )
distances <- unname( apply( a, 1, function(x)
abs(sum(x*pnorm)+D)/pnorm.len ) )
r[ft] <- distances
}
r
}
#' Distance to a Reference Point
#'
#' Compute the distance between the starting point of a track and a reference point.
#' Useful to
#' detect directed movement towards a point (see examples).
#'
#' @param x a single input track; a matrix whose first column is time and whose
#' remaining columns are a spatial coordinate.
#' @param p numeric vector of coordinates of the reference point p to compute distances to.
#' @param from index, or vector of indices, of the first row of the track. If
#' \code{from} is a vector, distances are returned for all steps starting at
#' the indices in \code{from}.
#'
#' @return A single distance.
#'
#' @seealso \code{\link{angleToPoint}} to compute the angle to the reference point, and
#' \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Plotting the angle versus the distance to a reference point can be informative to
#' ## detect biased movement towards that point. We should be suspicious especially
#' ## when small angles are more frequent at lower distances.
#' steps <- subtracks( Neutrophils, 1 )
#' bb <- boundingBox( Neutrophils )
#' angles <- sapply( steps, angleToPoint, p = bb["max",-1] )
#' distances <- sapply( steps, distanceToPoint, p = bb["max",-1] )
#' scatter.smooth( distances, angles )
#' abline( h = 90, col = "red" )
#' @export
distanceToPoint <- function (x, p = c(0,0,0), from = 1 )
{
# Check if the given point has the correct dimensions
if( length(p) != ncol(x) - 1 ){
stop("In distanceToPoint: Reference point coordinates must have the same number of dimensions as coordinates in the tracking data.")
}
# Initialize a vector of NAs for every index in "from".
# This will be replaced if possible.
r <- rep(NA, length(from))
# To compute step distance to point we need at least one step (two timepoints).
ft <- from < nrow(x)
if( sum(ft) > 0 ){
# compute vectors of step starting points with the refpoint
b <- x[from[ft],-1,drop=FALSE]
b <- t( apply( b, 1, function(x) x - p ) )
# Compute length of this vector (= distance)
blen <- sqrt(.rowSums(b^2, nrow(b),ncol(b)))
r[ft] <- blen
}
r
}
#' Distance between pairs of tracks at every timepoint
#'
#' For every timepoint in the dataset, compute pairwise distances between coordinates.
#'
#' @param X a tracks object
#' @param searchRadius if specified, return only pairs that are within this distance of each other.
#' Defaults to \code{Inf}, so if left unspecified, all pairs are returned.
#' @param times (optional) a vector of timePoints to check pairs at; by default this is just everything.
#' @param quietly (default FALSE) if TRUE, suppress warnings when there are no tracks with
#' overlapping timepoints and an empty dataframe is returned.
#'
#' @return a dataframe with the following columns:
#' \describe{
#' \item{cell1}{the id of the track to which the first coordinate belongs}
#' \item{cell2}{the id of the track to which the second coordinate belongs}
#' \item{t}{the time point at which their distance is assessed}
#' \item{dist}{the distance between the coordinates at this time}
#' }
#'
#' @examples
#' ## compute find timepoints where two t cells are within 1 micron of each other.
#' pairsByTime( TCells, searchRadius = 1 )
#'
#' ## indeed, the following two cells nearly touch:
#' plot( TCells[ c("24","9258") ] )
#' @export
pairsByTime <- function( X, searchRadius = Inf, times = timePoints(X), quietly = FALSE )
{
# check X tracks object
if( !is.tracks( X ) ) stop( "X must be a tracks object!" )
if( length(X) == 0 ){
if( !quietly ) warning( "pairsByTime: tracks object X is empty. Returning an empty dataframe." )
return( data.frame() )
}
# check times; must be a subset of timePoints(X).
if( !all( times %in% timePoints(X) ) ) stop( "times must all occur in X!" )
# tracks to dataframe, split into a list with one dataframe per
# timepoint. In that dataframe, set the cellid as rownames and
# keep only the coordinates.
df <- as.data.frame( X )
dataByTime <- split( df, df$t )
# filter data for the timepoints in 'times' (by default, that's everything)
dataByTime <- dataByTime[ as.character( times ) ]
coordsByTime <- lapply( dataByTime, function(x){
rownames(x) <- x$id
keep.cols <- !is.element( colnames(x), c("id"))
x <- x[,keep.cols]
return(x)
})
# Per timepoint in the list, compute distance matrix of all the cells present
# at that time, and then return pairwise distances in a dataframe.
pbt <- lapply( coordsByTime, function(x){
# distance from distance matrix
# (get coordinates of all points at this time; compute distance matrix)
coords <- x[,colnames(x)!="t"]# remove first column containing time.
dm <- as.matrix( stats::dist( coords ))
# rownames contain cell ids. Get all pairs with combn. This works only if
# there are at least two ids; return an empty dataframe otherwise.
if( length( rownames(dm) ) > 1 ){
pairs <- as.data.frame( t(utils::combn( rownames(dm), 2 )) )
colnames(pairs) <- c( "cell1","cell2" )
# add time info
pairs$t <- x[1,"t"]
pairs$dist <- apply( pairs, 1, function(y) dm[ y[1], y[2] ] )
# filter pairs within distance searchRadius of each other
pairs <- pairs[ pairs$dist <= searchRadius, ]
} else {
pairs <- data.frame( cell1 = character(0), cell2 = character(0), t = numeric(0), dist = numeric(0) )
}
return(pairs)
})
out <- do.call( rbind, pbt )
if( nrow(out) > 0 ){
rownames(out) <- paste0( out[,1], "-", out[,2], "-", out[,3] )
} else{
if( !quietly ) warning( "pairsByTime: no tracks share time points; returning an empty dataframe.")
}
return( out )
}
#' Angle between Two Steps
#'
#' Compute the angle between two steps in the dataset that occur at the same timepoint.
#'
#' @param X a tracks object
#' @param trackids a vector of two indices specifying the tracks to get steps from, or
#' a dataframe/matrix of two columns (where every row contains a pair of trackids to compute
#' a step angle for)
#' @param t the timepoint at which the steps should start, or a vector of timepoints if trackids
#' is a matrix with multiple step pairs to compute angles for.
#' @param degrees logical; should angle be returned in degrees instead of radians? (defaults to \code{TRUE})
#' @param quietly logical; should a warning be returned if one or both of the steps are missing
#' in the data and the function returns NA?
#'
#' @return A single angle, or NA if the desired step is missing for one or both
#' of the tracks. If trackids is a matrix with multiple step pairs to compute angles for,
#' the output is a numeric vector of angles (or NA values).
#'
#' @seealso \code{\link{distanceSteps}} to compute the distance between the step starting
#' points, \code{\link{timePoints}} to list all timepoints in a dataset,
#' and \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Find the angle between the steps of the tracks with ids 1 and 2, at the 3rd
#' ## timepoint in the dataset.
#' t <- timePoints( TCells )[3]
#' angleSteps( TCells, c("1","3"), t )
#'
#' ## Do this for multiple pairs and times at once: between cells 1 and 3 at the
#' ## 3rd timepoint, and between 1 and 4 at the fourth timepoint.
#' pairs <- data.frame( cell1 = c("1","1"), cell2 = c("3","4"))
#' times <- timePoints(TCells)[3:4]
#' angleSteps( TCells, pairs, times )
#' @export
angleSteps <- function( X, trackids, t, degrees = TRUE, quietly = FALSE )
{
# ---- Checking/handling input
if( !is.tracks(X) ){
stop( "angleSteps: X must be a tracks object." )
}
if( any( !is.element( t, timePoints(X) ) ) ){
stop( "angleSteps: the supplied timepoints must correspond to those in the data!")
}
if( is.data.frame(trackids)) trackids <- as.matrix( trackids )
if( is.matrix( trackids ) ){
if( ncol(trackids) != 2 ){
stop( "angleSteps: an angle is only defined for exactly 2 steps. Please provide exactly 2 trackids, or a matrix with 2 trackid columns per row.")
}
if( nrow( trackids ) != length(t) ){
stop( "angleSteps: if angles are to be computed for a matrix of trackid pairs, then t must be a vector with the same length as the number of pairs.")
}
} else {
if( !length(trackids) == 2 ){
stop( "angleSteps: an angle is only defined for exactly 2 steps. Please provide exactly 2 trackids.")
}
}
if( any( !is.element( trackids, names(X) ) ) ){
stop( "angleSteps: cannot find all supplied trackids in the data.")
}
# if trackids is not a matrix (so we're checking just one angle),
# then just make it a matrix of 1 row.
if( !is.matrix(trackids ) ) trackids <- matrix( trackids, ncol = length( trackids ) )
# ---- Compute angle(s)
# To compute angle(s), first restructure tracks to a dataframe; this will be faster
# because it will allow to compute multiple angles at once using matrix-based computations.
df <- as.data.frame(X)
# Add a column with the index of the timepoint (rather than absolute time);
# used below to find 'steps' in the data from coordinates at subsequent timepoints.
tsort <- sort( timePoints(X) )
tIndices <- stats::setNames( seq_along( tsort ), tsort )
df$timeIndex <- tIndices[ as.character( df$t ) ]
# set rownames of the data to a tag containing the cellid and the timepoint; this will
# allow us to find coordinates rapidly below.
rownames(df) <- paste0( df$id, "-", df$timeIndex )
# convert the timepoints of interest to time indices.
tindexVec <- tIndices[ as.character(t) ]
# Now find all relevant coordinates of the step pairs at the indicated time point.
# In trackids, each row is a pair with the two trackids in the columns. For both cells
# (columns), first find its coordinates at time t (="start") by using the rownames in df;
# discard t/id columns to keep coordinates only.
coordCols <- !is.element( colnames(df), c("t","id","timeIndex"))
start1 <- df[ paste0( trackids[,1], "-", unname( tindexVec ) ), coordCols ]
start2 <- df[ paste0( trackids[,2], "-", unname( tindexVec ) ), coordCols ]
# also find the coordinates at the next timepoint ( = "end" of the step). Note that this
# is simply the point with a time-index incremented by 1:
end1 <- df[ paste0( trackids[,1], "-", unname( tindexVec+1 ) ), coordCols ]
end2 <- df[ paste0( trackids[,2], "-", unname( tindexVec+1 ) ), coordCols ]
# check for which ids all relevant coordinates are actually found
keep <- ( rowSums( is.na( start1 ) ) == 0 ) & ( rowSums( is.na( start2 ) ) == 0 ) & ( rowSums( is.na( end1 ) ) == 0 ) & ( rowSums( is.na( end2 ) )== 0 )
# subtract start from end to get displacement vector
d1 <- as.matrix( end1[keep,] - start1[keep,] )
d2 <- as.matrix( end2[keep,] - start2[keep,] )
# init angles with NA (the default for if steps not in data), then add angles where possible
ang <- rep( NA, nrow( trackids ) )
if( sum( keep ) > 0 ) ang[keep] <- vecAngle( d1, d2, degrees = degrees )
# warn if NAs left
if( any( is.na( ang ) ) && !quietly ){
warning( "Warning: for some pairs I cannot find data for both steps at the indicated time. Returning NA.")
}
ang
}
#' Distance between Two Steps
#'
#' Compute the distance between two steps in the dataset that occur at the same timepoint.
#' The distance is the distance between the step starting points.
#'
#' @param X a tracks object
#' @param trackids a vector of two indices specifying the tracks to get steps from, or
#' a dataframe/matrix of two columns (where every row contains a pair of trackids to compute
#' a step angle for)
#' @param t the timepoint at which the steps should start, or a vector of such timepoints if
#' multiple step pairs are supplied in \code{trackids}.
#' @param quietly logical; should a warning be returned if one or both of the steps are missing
#' in the data and the function returns NA?
#'
#' @return A single distance (NA if the desired timepoint is missing for one or both
#' of the tracks), or a vector of such distances if multiple step pairs are supplied in \code{trackids}.
#'
#' @seealso \code{\link{angleSteps}} to compute the angle between the steps,
#' \code{\link{timePoints}} to list all timepoints in a dataset,
#' and \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Find the distance between the steps of the tracks with ids 1 and 3, at the 3rd
#' ## timepoint in the dataset.
#' t <- timePoints( TCells )[3]
#' distanceSteps( TCells, c("1","3"), t )
#'
#' ## Do this for multiple pairs and times at once: between cells 1 and 3 at the
#' ## 3rd timepoint, and between 1 and 4 at the fourth timepoint.
#' pairs <- data.frame( cell1 = c("1","1"), cell2 = c("3","4"))
#' times <- timePoints(TCells)[3:4]
#' distanceSteps( TCells, pairs, times )
#' @export
distanceSteps <- function( X, trackids, t, quietly = FALSE )
{
# ---- Checking/handling input
if( !is.tracks(X) ){
stop( "distanceSteps: X must be a tracks object." )
}
if( any( !is.element( t, timePoints(X) ) ) ){
stop( "distanceSteps: the supplied timepoints must correspond to those in the data!")
}
if( is.data.frame(trackids)) trackids <- as.matrix( trackids )
if( is.matrix( trackids ) ){
if( ncol(trackids) != 2 ){
stop( "distanceSteps: a distance is only defined for exactly 2 steps. Please provide exactly 2 trackids, or a matrix with 2 trackid columns per row.")
}
if( nrow( trackids ) != length(t) ){
stop( "distanceSteps: if distances are to be computed for a matrix of trackid pairs, then t must be a vector with the same length as the number of pairs.")
}
} else {
if( !length(trackids) == 2 ){
stop( "distanceSteps: only defined for exactly 2 steps. Please provide exactly 2 trackids.")
}
}
if( any( !is.element( trackids, names(X) ) ) ){
stop( "distanceSteps: cannot find all supplied trackids in the data.")
}
# if trackids is not a matrix (so we're checking just one angle),
# then just make it a matrix of 1 row.
if( !is.matrix(trackids ) ) trackids <- matrix( trackids, ncol = length( trackids ) )
# ---- Compute distance(s)
# Compute end times of the steps
tsort <- sort( timePoints(X) )
tIndices <- stats::setNames( seq_along( tsort ), tsort )
tIndexVec <- tIndices[as.character(t)]
tNext <- tsort[ tIndexVec + 1 ]
# check for which pair/time combinations step data are actually present
df <- as.data.frame( X )
rownames(df) <- paste0( df$id, "-", df$t )
coordCols <- !is.element( colnames(df), c("t","id") )
start1 <- df[ paste0( trackids[,1], "-", t ), coordCols ]
start2 <- df[ paste0( trackids[,2], "-", t ), coordCols ]
end1 <- df[ paste0( trackids[,1], "-", tNext ), coordCols ]
end2 <- df[ paste0( trackids[,2], "-", tNext ), coordCols ]
keep <- ( rowSums( is.na( start1 ) ) == 0 ) & ( rowSums( is.na( start2 ) ) == 0 ) & ( rowSums( is.na( end1 ) ) == 0 ) & ( rowSums( is.na( end2 ) )== 0 )
# for the pairs where this is the case, we can get the distance using pairsByTime, using
# the starting time of the step. For other pairs, we'll return NA, so init this first.
distances <- rep( NA, nrow( trackids ) )
if( sum( keep ) > 0 ){
tvec <- intersect( unique( c( t, tNext ) ), timePoints( X ) )
pbt <- pairsByTime( X, times = tvec )
#trackids must be sorted with lowest first
trackids <- t( apply( trackids, 1, sort ) )
distances[keep] <- pbt[ paste0( trackids[keep,1], "-", trackids[keep,2], "-", t[keep] ),"dist" ]
}
# if trackids are equal their distance is zero.
same <- ( trackids[,1] == trackids[,2] )
distances[same] <- 0
if( any(is.na(distances)) ){
if(!quietly){warning( "Warning: cannot find data for both steps. Returning NA.")}
}
return(distances)
}
#' Find Pairs of Steps Occurring at the Same Time
#'
#' Find cell indices and timepoints where these cells both have a step.
#'
#' @param X a tracks object
#' @param filter.steps optional: a function used to filter steps on. See examples.
#'
#' @return A dataframe with three columns: two for the indices of cellpairs that
#' share a step, and one for the timepoint at which they do so.
#'
#' @examples
#' ## Find all pairs of steps in the T cell data that displace at least 2 microns.
#' pairs <- stepPairs( TCells, filter.steps = function(t) displacement(t) > 2 )
#'
#' @export
stepPairs <- function( X, filter.steps=NULL )
{
if( !is.tracks(X) ){
stop( "stepPairs: X must be a tracks object!" )
}
dout <- data.frame()
for( t in timePoints(X) ){
# All steps starting at that timepoint
xt <- subtracksByTime( X, t, 1 )
# Filter out steps that do not fulfill the filter criterion
if( length(xt) > 0 && is.function( filter.steps ) ){
xt <- xt[ sapply(xt,filter.steps ) ]
}
# Extract their ids
ids <- names(xt)
# Make all possible pairs
if( length(ids) >= 2 ){
pairs <- as.data.frame( t( utils::combn( ids, 2 ) ),
stringsAsFactors = FALSE )
colnames(pairs) <- c( "p1","p2" )
# To a dataframe
pairs$t <- t
dout <- rbind( dout, pairs )
}
}
dout
}
#' Find Distances and Angles for all Pairs of Steps
#'
#' Find cell indices and timepoints where these cells both have a step, then return
#' angles and distances for each pair of steps.
#'
#' @param X a tracks object
#' @param searchRadius if specified, only return analysis for pairs of steps that
#' start within distance searchRadius from each other
#' @param filter.steps optional: a function used to filter steps on. See examples.
#' @param quietly (default FALSE) if TRUE, suppress warnings
#' @param ... further arguments passed on to \code{angleSteps}
#'
#' @return A dataframe with five columns: two for the indices of cellpairs that
#' share a step, one for the timepoint at which they do so, one for the distance
#' between them, and one for their angle.
#'
#' @details Analyzing step angles at different distances can be useful to detect
#' directional bias or local crowding effects; see (Beltman et al, 2009).
#'
#' Internally, the function uses \code{\link{stepPairs}}, \code{\link{angleSteps}},
#' and \code{\link{distanceSteps}}.
#'
#' @seealso \code{\link{analyzeCellPairs}} to do something similar for entire tracks
#' rather than single steps.
#'
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
#'
#' @examples
#' ## Plot distance versus angle for all step pairs, filtering for those that
#' ## displace at least 2 microns. Sample dataset in this example for speed.
#' pairs <- analyzeStepPairs( sample( TCells, 100), filter.steps = function(t) displacement(t) > 2 )
#' scatter.smooth( pairs$dist, pairs$angle )
#' @export
analyzeStepPairs <- function( X, filter.steps = NULL, searchRadius = Inf, quietly = FALSE, ... )
{
if( !is.tracks(X) ){
stop( "analyzeStepPairs: X must be a tracks object!" )
}
# Obtain cell pairs for each timepoint; using filter.steps as filter criterion.
pairs <- stepPairs( X, filter.steps = filter.steps )
if( nrow(pairs) == 0 ) return(pairs)
# initialise dist/angle with NA. Set rownames p1-p2-t for lookup later.
pairs$dist <- NA
pairs$angle <- NA
rownames(pairs) <- paste0( pairs$p1, "-", pairs$p2, "-", pairs$t )
# pairs of cells by time coordinate; this also computes a distance.
# these data have rownames id1-id2-t for matching with the pairs dataframe later.
pByTime <- pairsByTime( X, searchRadius = searchRadius, quietly = quietly )
# add angles for these pairs (which are within the search radius).
pByTime$angle <- angleSteps( X, as.matrix(pByTime[,1:2]), pByTime[,3], quietly = TRUE, ... )
# Note that pByTime contains all pairs that co-occur at some timepoint, but that
# for analyzing steps they need to co-occur at least at two subsequent timepoints.
# Thus, pByTime may contain pairs not included in the 'pairs' dataframe generated
# by stepPairs() above. The same holds true if some filter.steps criterion is specified.
# But it is also possible that there are pairs in 'pairs' which are not in pByTime,
# since pByTime is already focused on pairs within distance searchRadius from each other.
# therefore intersect to keep only the pairs that fulfill all these criteria.
pairs <- pairs[ intersect( rownames(pairs), rownames( pByTime ) ), ]
# Now that we are sure all pairs in 'pairs' are also in 'pByTime', lookup the computed
# angles and distances there and return.
pairs$dist <- pByTime[ rownames(pairs), "dist" ]
pairs$angle <- pByTime[ rownames(pairs), "angle" ]
pairs
}
#' Find Distances and Angles for all Pairs of Tracks
#'
#' Find all pairs of cells and return the shortest distance between them at any
#' point in time (if they share any time points), as well as the angle between
#' their overall displacement vectors.
#'
#' @param X a tracks object
#' @param searchRadius if specified, only return analysis for pairs of cells that
#' are within distance searchRadius from each other at least at one point in time.
#' @param quietly (default FALSE) if TRUE, suppress warnings
#' @param ... further arguments passed on to \code{angleCells}
#'
#' @return A dataframe with four columns: two for the indices of cellpairs,
#' one for the distance between them, and one for their angle. Note that the
#' distance will be NA for pairs of tracks that do not share time points, but
#' their angle will still be computed.
#'
#' @details Analyzing track angles at different distances can be useful to detect
#' directional bias or local crowding effects; see (Beltman et al, 2009).
#'
#' Internally, the function uses \code{\link{cellPairs}}, \code{\link{angleCells}},
#' and \code{\link{distanceCells}}.
#'
#' @seealso \code{\link{analyzeStepPairs}} to do something similar for single steps
#' rather than entire tracks.
#'
#'
#' @references
#' Joost B. Beltman, Athanasius F.M. Maree and Rob. J. de Boer (2009),
#' Analysing immune cell migration. \emph{Nature Reviews Immunology} \bold{9},
#' 789--798. doi:10.1038/nri2638
#'
#' @examples
#' ## Plot distance versus angle for all cell pairs. Sample T-cell data here for speed.
#' pairs <- analyzeCellPairs( sample( TCells, 100 ) )
#' scatter.smooth( pairs$dist, pairs$angle )
#' @export
analyzeCellPairs <- function( X, searchRadius = Inf, quietly = FALSE, ... )
{
if( !is.tracks(X) ){
stop( "analyzeCellPairs: X must be a tracks object!" )
}
# Make all possible pairs of cellids
pairs <- cellPairs( X )
if( nrow( pairs ) == 0 ) return(pairs)
# init distances and angles with NA.
pairs$dist <- NA
pairs$angle <- NA
# per timepoint, compute distances for all pairs of cells that
# are both present at that time point. Output is a df with cell ids, time, and distance.
# Note that multiple distances per pair are present if the cells co-occur at multiple
# timepoints.
allPairs <- pairsByTime( X, searchRadius = searchRadius, quietly = quietly )
if( nrow( allPairs ) == 0 ){
return( data.frame( cell1 = character(0), cell2 = character(0), dist = numeric(0), angle = numeric(0) ))
}
# Split by pair to find the min distance between the cells over time; then merge again
# into a single dataframe.
distByPair <- split( allPairs, paste0( allPairs$cell1, "-", allPairs$cell2 ) )
minDistByPair <- lapply( distByPair, function(x) {
minD <- min( x$dist )
x <- x[1,]
x$dist <- minD
return(x)
})
pairMinDist <- do.call( rbind, minDistByPair )
# the pairMinDist already has rownames in the form of 'id1-id2' so data can be accessed easily.
# do the same with the 'pairs' dataframe of query points generated above:
rownames( pairs ) <- paste0( pairs$cell1, "-", pairs$cell2 )
# For all the query points, get the distance and angle from the pairMinDist dataframe.
# Some query points will not be in this dataframe (if cells do not co-occur at any time);
# in that case, dist will remain NA.
pairs[ rownames( pairMinDist ), "dist" ] <- pairMinDist$dist
# add angles and return
pairs$angle <- angleCells( X, pairs[,1:2], ...)
return(pairs)
}
#' Find Pairs of Tracks
#'
#' Get all unique combinations of two track ids.
#'
#' @param X a tracks object
#'
#' @return A dataframe with two columns: one for each of the track ids in the pair.
#' Each row represents a pair.
#'
#' @examples
#' ## Find all pairs of cells in the T cell data
#' pairs <- cellPairs( TCells )
#' @export
cellPairs <- function( X )
{
if( !is.tracks(X) ){
stop( "cellPairs: X must be a tracks object!" )
}
cellids <- names( X )
pairs <- data.frame()
if( length(cellids) >= 2 ){
# Make all possible pairs of cellids
pairs <- as.data.frame( t( utils::combn( cellids, 2 ) ),
stringsAsFactors = FALSE )
colnames(pairs) <- c( "cell1","cell2" )
}
return(pairs)
}
#' Angle between Two Tracks
#'
#' Compute the angle between the displacement vectors of two tracks in the dataset,
#' or of several such pairs at once.
#' Note that in contrast to \code{\link{distanceCells}}, this angle is computed even
#' when the two tracks do not share any time points.
#'
#' @param X a tracks object
#' @param cellids a vector of two indices specifying the tracks to get steps from, or
#' a dataframe/matrix of two columns (where every row contains a pair of cellids to compute
#' an angle for)
#' @param degrees logical; should angle be returned in degrees instead of radians? (defaults to \code{TRUE})
#'
#' @return A single angle (if two cellids given), or a vector of angles (if multiple pairs of cellids are supplied).
#'
#' @seealso \code{\link{distanceCells}} to compute the minimum distance between the tracks,
#' and \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Find the angle between the tracks with ids 1 and 3
#' angleCells( TCells, c("1","3") )
#'
#' ## Find the angles of several cell pairs at once
#' pairs <- data.frame( cell1 = c("1","1"), cell2 = c( "3","4" ) )
#' angleCells( TCells, pairs )
#' @export
angleCells <- function (X, cellids, degrees = TRUE )
{
#--- check/handle inputs
if (!is.tracks(X)) {
stop("angleCells: X must be a tracks object!")
}
if( is.data.frame(cellids)) cellids <- as.matrix( cellids )
unique.ids <- unique( as.vector( cellids ))
if (any(!is.element(unique.ids, names(X)))) {
stop("angleCells: cannot find all cellids in data.")
}
if( is.matrix( cellids ) ){
if( ncol(cellids) != 2 ){
stop( "angleCells: an angle is only defined for exactly 2 cells. Please provide exactly 2 cellids, or a matrix with 2 trackid columns per row.")
}
} else {
if( !length(cellids) == 2 ){
stop( "angleCells: an angle is only defined for exactly 2 cells. Please provide exactly 2 cellids.")
}
}
# if cellids is not a matrix (so we're checking just one angle),
# then just make it a matrix of 1 row.
if( !is.matrix(cellids ) ) cellids <- matrix( cellids, ncol = length(cellids ) )
#--- compute
# Get displacement vectors for all the ids in trackids
disps <- lapply( unique( as.vector( cellids ) ), function(x) displacementVector( X[[x]] ) )
names(disps) <- unique( as.vector( cellids ) )
# Get a matrix of displacement vectors for the ids in the first column of trackids,
# and likewise for the second id of each pair
disps1 <- do.call( rbind, disps[ cellids[,1] ] )
disps2 <- do.call( rbind, disps[ cellids[,2] ] )
# compute all angles at once using these matrices.
return(vecAngle(disps1, disps2, degrees = degrees))
}
#' Minimum Distance between Two Cells
#'
#' Compute the minimum distance between two cells in the dataset (minimum over all)
#' the timepoints where they were both measured.
#'
#' @param X a tracks object
#' @param cellids a vector of two indices specifying the tracks to compute distance between, or
#' a dataframe/matrix of two columns (where every row contains a pair of cellids to compute
#' a distance for)
#' @param quietly if TRUE, suppress warnings about returning NA distances.
#'
#' @return A single distance (NA if the the tracks do not have overlapping timepoints), or
#' a vector of such distances if multiple pairs are supplied in \code{cellids}.
#'
#' @seealso \code{\link{angleCells}} to compute the angle between the track displacement vectors,
#' and \code{\link{AngleAnalysis}} for other methods to compute angles and distances.
#'
#' @examples
#' ## Find the minimum distance between the tracks with ids 1 and 3
#' distanceCells( TCells, c("1","3") )
#' @export
distanceCells <- function( X, cellids, quietly = FALSE )
{
if( !is.tracks(X) ){
stop( "distanceCells: X must be a tracks object!" )
}
if( is.data.frame(cellids)) cellids <- as.matrix( cellids )
if( any( !is.element( unique( as.vector( cellids ) ) , names(X) ) ) ){
stop( "distanceCells: cannot find both cellids in data." )
}
if( is.matrix( cellids ) ){
if( ncol(cellids) != 2 ){
stop( "distanceCells: a distance is only defined for exactly 2 cells. Please provide exactly 2 cellids, or a matrix with 2 trackid columns per row.")
}
} else {
if( !length(cellids) == 2 ){
stop( "distanceCells: a distance is only defined for exactly 2 cells. Please provide exactly 2 cellids.")
}
}
# if cellids is not a matrix (so we're checking just one angle),
# then just make it a matrix of 1 row.
if( !is.matrix(cellids ) ) cellids <- matrix( cellids, ncol = length(cellids ) )
# Sort cellids per row
cellids <- t( apply( cellids, 1, sort ) )
# initalize distance as NA.
pairs <- paste0( cellids[,1],"-", cellids[,2] )
distances <- stats::setNames( rep( NA, length(pairs)), pairs )
# for any pairs that share timepoints, add the distance.
# pairsByTime returns distances for all timepoints where both tracks are present;
# use this to extract the min distance for each pair.
pbt <- pairsByTime( X[ unique( as.vector( cellids ) ) ], quietly = TRUE )
if( nrow(pbt) > 0 ){
pbt[,1:2] <- t( apply( pbt[,1:2], 1, sort ) )
pbtMin <- sapply( split( pbt, paste0( pbt$cell1, "-", pbt$cell2 ) ), function(x) min( x$dist ) )
add.pairs <- pairs[ is.element( pairs, names( pbtMin ))]
distances[add.pairs] <- unname( pbtMin[add.pairs] )
}
# if cellids are equal, distance is zero
distances[ cellids[,1]==cellids[,2]] <- 0
# Warning if there are NAs
if( any(is.na(distances)) ){
if(!quietly){warning( "Warning: distance undefined for cells that don't share timepoints; returning NA.")}
}
return(unname( distances) )
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.