R/DropTip.R

Defines functions KeepTipPostorder KeepTipPreorder KeepTip.NULL DropTip.NULL KeepTip.list DropTip.list KeepTip.multiPhylo DropTip.multiPhylo .DescendantTips DropTip.Splits KeepTip.phylo DropTip.phylo KeepTip DropTip

Documented in DropTip DropTip.list DropTip.multiPhylo DropTip.NULL DropTip.phylo DropTip.Splits KeepTip KeepTip.list KeepTip.multiPhylo KeepTip.NULL KeepTip.phylo KeepTipPostorder KeepTipPreorder

#' Drop leaves from tree
#'
#' `DropTip()` removes specified leaves from a phylogenetic tree, collapsing
#' incident branches.
#'
#' This function differs from [`ape::drop.tip()`], which roots unrooted trees,
#' and which can crash when trees' internal numbering follows unexpected schema.
#'
#' @template treeParam
#' @param tip Character vector specifying labels of leaves in tree to be
#' dropped or kept, or integer vector specifying the indices of leaves to be
#' dropped or kept.
#' Specifying the index of an internal node will drop all descendants of that
#' node.
#' @param preorder Logical specifying whether to [Preorder] `tree` before
#' dropping tips.  Specifying `FALSE` saves a little time, but will result in
#' undefined behaviour if `tree` is not in preorder.
#' @param check Logical specifying whether to check validity of `tip`. If
#' `FALSE` and `tip` contains entries that do not correspond to leaves of the
#' tree, undefined behaviour may occur.
#'
#' @return `DropTip()` returns a tree of class `phylo`, with the requested
#' leaves removed. The edges of the tree will be numbered in preorder,
#' but their sequence may not conform to the conventions of [`Preorder()`].
#'
#' @examples
#' tree <- BalancedTree(9)
#' plot(tree)
#' plot(DropTip(tree, c("t5", "t6")))
#' 
#' unrooted <- UnrootTree(tree)
#' plot(unrooted)
#' plot(DropTip(unrooted, 4:5))
#'
#' @family tree manipulation
#' @template MRS
#' @export
DropTip <- function(tree, tip, preorder = TRUE, check = TRUE) {
  UseMethod("DropTip")
}

#' @rdname DropTip
#' @return `KeepTip()` returns `tree` with all leaves not in `tip` removed,
#' in preorder.
#' @export
KeepTip <- function(tree, tip, preorder = TRUE, check = TRUE) {
  UseMethod("KeepTip")
}

#' @rdname DropTip
#' @export
DropTip.phylo <- function(tree, tip, preorder = TRUE, check = TRUE) {
  if (preorder && (length(attr(tree, "order")) == 0 || 
                   attr(tree, "order")[[1]] != "preorder")) {
    tree <- Preorder(tree)
  }
  labels <- tree[["tip.label"]]
  nTip <- length(labels)
  
  # Establish which tips should be dropped
  if (is.null(tip) || !length(tip) || any(is.na(tip))) {
    drop <- logical(nTip)
  } else if (is.character(tip)) {
    if (check) {
      if (sum(unique(labels) %in% tip) != length(tip)) {
        warning(paste(tip[!tip %in% labels], collapse = ", "),
                " not present in tree")
      }
    }
    drop <- labels %in% tip
  } else if (is.numeric(tip)) {
    if (check) {
      nNodes <- nTip + tree[["Nnode"]]
      if (any(tip > nNodes)) {
        warning("Tree only has ", nNodes, " nodes")
        tip <- tip[tip <= nNodes]
      }
      if (any(tip < 1L)) {
        warning("`tip` must be > 0")
        tip <- tip[tip > 0L]
      }
      
      if (any(tip > nTip)) {
        edge <- tree[["edge"]]
        parent <- edge[, 1]
        child <- edge[, 2]
        drop <- tabulate(c(tip[tip <= nTip],
                           .DescendantTips(parent, child, nTip,
                                           tip[tip > nTip])),
                         nbins = nTip)
      } else {
        drop <- as.logical(tabulate(tip, nbins = nTip))
      }
    } else {
      drop <- as.logical(tabulate(tip, nbins = nTip))
    }
  } else if (is.logical(tip)) {
    if (check) {
      if (length(tip) != nTip) {
        stop("`tip` must list `TRUE` or `FALSE` for each leaf.")
      }
    }
    drop <- tip
  } else {
    stop("`tip` must be of type character or numeric")
  }
  
  # Having established which tips to drop,
  # work out what to do about it
  if (all(drop)) {
    return(structure(list(edge = matrix(0, 0, 2), tip.label = character(0),
                          Nnode = 0), class = "phylo"))
  }
  
  if (any(drop)) {
    weights <- tree[["edge.length"]]
    keep <- !drop
    nodeLabel <- tree[["node.label"]]
    if (!is.null(weights)) {
      original <- PathLengths(tree, fullMatrix = TRUE)
    }
    if (!is.null(weights) || !is.null(nodeLabel)) {
      verts <- KeptVerts(tree, keep)
      tree[["node.label"]] <- nodeLabel[verts[-seq_len(nTip)]]
    }
    tree[["edge"]] <- keep_tip(tree[["edge"]], keep)
    tree[["tip.label"]] <- labels[keep]
    tree[["Nnode"]] <- dim(tree[["edge"]])[1] + 1L - sum(keep)
    
    if (!is.null(weights)) {
      newVerts <- matrix(which(verts)[tree[["edge"]]], ncol = 2L)
      tree[["edge.length"]] <- original[newVerts]
    }
  }
  
  # Return:
  tree
}

#' @rdname DropTip
#' @export
KeepTip.phylo <- function(tree, tip, preorder = TRUE, check = TRUE) {
  drop <- switch(
    mode(tip),
    "logical" = {
      !tip
    }, "character" = {
      setdiff(TipLabels(tree), MatchStrings(tip, TipLabels(tree), warning))
    }, "numeric" = {
      nTip <- NTip(tree)
      nValid <- if (inherits(tree, "phylo")) {
        nTip + tree[["Nnode"]]
      } else {
        nTip
      }
      badTips <- tip > nValid | tip < 1
      if (any(badTips)) {
        warning("`tip` must be between 1 and ", nValid)
        tip <- tip[!badTips]
      }
      nodes <- tip > nTip
      keep <- if (any(nodes)) {
        edge <- tree[["edge"]]
        theNodes <- tip[nodes]
        nodeDesc <- DescendantTips(edge[, 1], edge[, 2], node = theNodes)
        union(tip[!nodes], which(
          if (is.null(dim(nodeDesc))) nodeDesc else colSums(nodeDesc) > 0))
      } else {
        tip
      }
      setdiff(seq_len(nTip), keep)
    },
    stop("`tip` must be of type character, logical or numeric")
  )
  DropTip(tree, drop, preorder, check)
}

#' @rdname DropTip
#' @export
KeepTip.Splits <- KeepTip.phylo

#' @rdname DropTip
#' @examples summary(DropTip(as.Splits(tree), 4:5))
#' @family split manipulation functions
#' @export
DropTip.Splits <- function(tree, tip, preorder, check = TRUE) {
  labels <- TipLabels(tree)
  if (is.null(tip) || !length(tip) || any(is.na(tip))) {
    return(tree)
  } else if (is.character(tip)) {
    if (check) {
      drop <- match(tip, labels)
      missing <- is.na(drop)
      if (any(missing)) {
        warning(paste(tip[missing], collapse = ", "), " not present in tree")
        drop <- drop[!missing]
      }
      drop <- as.logical(tabulate(drop, NTip(tree)))
    } else {
      drop <- labels %in% tip
    }
  } else if (is.numeric(tip)) {
    nTip <- NTip(tree)
    if (check) {
      if (any(tip > nTip)) {
        warning("Tree only has ", nTip, " leaves")
        tip <- tip[tip <= nTip]
      }
      if (any(tip < 1L)) {
        warning("`tip` must be > 0")
        tip <- tip[tip > 0L]
      }
    }
    drop <- as.logical(tabulate(tip, nTip))
  } else if (is.logical(tip)) {
    if (check && length(tip) != NTip(tree)) {
      stop("`tip` must contain an entry for each leaf of `tree`.")
    }
    drop <- tip
  } else {
    stop("`tip` must be of type character, logical or numeric")
  }
  
  keep <- !drop
  thinner <- structure(thin_splits(tree, drop),
                       nTip = sum(keep),
                       tip.label = labels[keep],
                       class = "Splits")
  
  # Return:
  unique(thinner, fromLast = TRUE)
}

# nodes must all be internal
.DescendantTips <- function(parent, child, nTip, nodes, isDesc = logical(nTip)) {
  newDescs <- child[parent %in% nodes]
  recurse <- newDescs > nTip
  
  # Return:
  if (any(recurse)) {
    isDesc[newDescs[!recurse]] <- TRUE
    .DescendantTips(parent, child, nTip, newDescs[recurse], isDesc)
  } else {
    isDesc[newDescs] <- TRUE
    which(isDesc)
  }
}

#' @describeIn DropTip Direct call to `DropTip.phylo()`, to avoid overhead of
#' querying object's class.
#' @export
DropTipPhylo <- DropTip.phylo

#' @rdname DropTip
#' @export
DropTip.multiPhylo <- function(tree, tip, preorder = TRUE, check = TRUE) {
  at <- attributes(tree)
  tree <- lapply(tree, DropTip, tip, preorder)
  attributes(tree) <- at
  if (!is.null(at[["TipLabel"]])) {
    attr(tree, "TipLabel") <- setdiff(at[["TipLabel"]], tip)
  }
  tree
}

#' @rdname DropTip
#' @export
KeepTip.multiPhylo <- function(tree, tip, preorder = TRUE, check = TRUE) {
  at <- attributes(tree)
  tree <- lapply(tree, KeepTip, tip = tip, preorder = preorder, check = check)
  attributes(tree) <- at
  if (!is.null(at[["TipLabel"]])) {
    attr(tree, "TipLabel") <- intersect(at[["TipLabel"]], tip)
  }
  tree
}

#' @rdname DropTip
#' @export
DropTip.list <- function(tree, tip, preorder = TRUE, check = TRUE) {
  if (all(vapply(tree, inherits, logical(1), "phylo"))) {
    DropTip.multiPhylo(tree, tip, preorder, check)
  } else {
    stop("Expected a list of `phylo` objects, but not all elements inherit ",
         "from \"phylo\".\n  Classes found: ",
         paste(unique(vapply(tree, function(x) paste(class(x), collapse = "/"),
                             character(1))),
               collapse = ", "))
  }
}

#' @rdname DropTip
#' @export
KeepTip.list <- function(tree, tip, preorder = TRUE, check = TRUE) {
  if (all(vapply(tree, inherits, logical(1), "phylo"))) {
    KeepTip.multiPhylo(tree, tip, preorder, check)
  } else {
    stop("Expected a list of `phylo` objects, but not all elements inherit ",
         "from \"phylo\".\n  Classes found: ",
         paste(unique(vapply(tree, function(x) paste(class(x), collapse = "/"),
                             character(1))),
               collapse = ", "))
  }
}

#' @rdname DropTip
#' @export
DropTip.NULL <-  function(tree, tip, preorder = TRUE, check = TRUE) {
  NULL
}

#' @rdname DropTip
#' @export
KeepTip.NULL <-  function(tree, tip, preorder = TRUE, check = TRUE) {
  NULL
}

#' @describeIn DropTip Faster version with no checks.
#' Does not retain labels or edge weights.
#' Edges must be listed in preorder.
#' May crash if improper input is specified.
#' @export
KeepTipPreorder <- function(tree, tip) {
  if (!any(tip)) {
    structure(list(edge = matrix(0, 0, 2), tip.label = character(0),
                          Nnode = 0), class = "phylo")
  } else if (all(tip)) {
    tree
  } else {
    tree[["edge"]] <- keep_tip(tree[["edge"]], tip)
    tree[["tip.label"]] <- as.character(which(tip))
    tree[["Nnode"]] <- dim(tree[["edge"]])[1] + 1L - sum(tip) 
    tree
  }
}
  
#' @describeIn DropTip Faster version with no checks.
#' Does not retain labels or edge weights.
#' Edges must be listed in postorder.
#' May crash if improper input is specified.
#' @export
KeepTipPostorder <- function(tree, tip) {
  if (!any(tip)) {
    structure(list(edge = matrix(0, 0, 2), tip.label = character(0),
                          Nnode = 0), class = "phylo")
  } else if (all(tip)) {
    tree
  } else {
    edge <- tree[["edge"]]
    tree[["edge"]] <- keep_tip(edge[dim(edge)[1]:1, ], tip)
    tree[["tip.label"]] <- tree[["tip.label"]][tip]
    tree[["Nnode"]] <- dim(tree[["edge"]])[1] + 1L - sum(tip) 
    tree
  }
}

Try the TreeTools package in your browser

Any scripts or data that you put into this service are public.

TreeTools documentation built on April 23, 2026, 5:06 p.m.