Nothing
# Input/output related functions
#' \code{writeFasta} Write a fasta file of sequences given a
#' named list of sequences
#' @param seqs named list of sequences (output from \code{readFasta})
#' @param file FASTA file for output
#'
#' @return File of FASTA formatted sequences
#' @export
writeFasta <- function(seqs, file){
if(!is.null(seqs)){
out <- paste0(">", names(seqs), "\n", seqs)
writeLines(out, con=file)
}else{
file.create(file)
}
}
# Write a clone's sequence alignment to a fasta file
#
# \code{cloneToFasta} write clone sequences as a fasta file
# @param c airrClone object
# @param fastafile file to be exported
# @param germid sequence id of germline
# @param trait trait to include in sequence ids
# @param empty don't include real sequence information
#
# @return Name of exported fasta file.
cloneToFasta <- function(c, fastafile, germid, trait=NULL, empty=FALSE){
text <- ""
if(!is.null(trait)){
c@data$sequence_id <- paste(c@data$sequence_id,c@data[,trait],sep="_")
}
for(i in 1:nrow(c@data)){
text <- paste0(text,">",c@data[i,]$sequence_id,"\n")
if(!empty){
if(c@phylo_seq == "sequence"){
text <- paste0(text,c@data[i,]$sequence,"\n")
}else if(c@phylo_seq == "lsequence"){
text <- paste0(text,c@data[i,]$lsequence,"\n")
}else if(c@phylo_seq == "hlsequence"){
text <- paste0(text,c@data[i,]$hlsequence,"\n")
}else{
stop(paste("phylo_seq not recognized",c@clone))
}
}else{
text <- paste0(text,"ATG\n")
}
}
text <- paste0(text,">",germid,"\n")
if(!empty){
if(c@phylo_seq == "sequence"){
text <- paste0(text,c@germline,"\n")
}else if(c@phylo_seq == "lsequence"){
text <- paste0(text,c@lgermline,"\n")
}else if(c@phylo_seq == "hlsequence"){
text <- paste0(text,c@hlgermline,"\n")
}else{
stop(paste("phylo_seq not recognized",c@clone))
}
}else{
text <- paste0(text,"ATG\n")
}
write(text,file=fastafile,append=FALSE)
return(fastafile)
}
#' Write a fasta file of sequences
#' \code{readFasta} reads a fasta file
#' @param df dataframe of sequences
#' @param id Column name of sequence ids
#' @param seq Column name of sequences
#' @param file FASTA file for output
#' @param imgt_gaps Keep IMGT gaps if present?
#' @param columns vector of column names to append to sequence id
#'
#' @return File of FASTA formatted sequences
#' @export
dfToFasta <- function(df, file, id="sequence_id", seq="sequence",
imgt_gaps=FALSE, columns=NULL){
if(!"data.frame" %in% class(df)){
stop("df must be a data.frame or tibble")
}
if(!id %in% names(df)){
stop(id, " column not found in df")
}
if(!seq %in% names(df)){
stop(seq, " column not found in df")
}
if(!imgt_gaps){
seqs <- gsub("\\.","",df[[seq]])
}else{
seqs <- df[[seq]]
}
if(is.null(columns)){
ids <- df[[id]]
}else{
if(sum(!columns %in% names(df)) > 0){
nf <- columns[!columns %in% names(df)]
stop(paste(nf, collapse=","), " not found in df")
}
ids <- df[[id]]
for(column in columns){
values <- paste0("|", column, "=", df[[column]])
ids <- paste0(ids, values)
}
}
lines <- paste0(">", ids, "\n", seqs)
writeLines(lines, con=file)
}
#' Read a fasta file into a list of sequences
#' \code{readFasta} reads a fasta file
#' @param file FASTA file
#'
#' @return List of sequences
#' @export
readFasta <- function(file){
f <- readLines(file)
if(length(f) == 1){
return(NULL)
}
seqs <- list()
id <- NA
for(line in f){
if(grepl("^>",line)){
id <- gsub(">","",line)
seqs[[id]] <- ""
}else{
if(is.na(id)){
stop(paste("Error reading",file))
}
seqs[[id]] <- paste0(seqs[[id]],line)
}
}
seqs
}
#' Exports the phylogenetic trees from the airrClone object
#'
#' \code{exportTrees} Exports phylogenetic trees
#' @param clones tibble \code{airrClone} objects, the output of
#' \link{getTrees} or \code{getTimeTrees}
#' @param filepath The file path for where the trees will be saved
#' @param tree_column The name of the column that contains the trees
#' @param ... additional arguments to be passed
#'
#' @export
exportTrees <- function(clones, filepath, tree_column = "trees", ...){
# check to see if the trees column is there
if(alakazam::checkColumns(clones, tree_column)){
if(inherits(clones[[tree_column]][[1]], "phylo")){
ape::write.tree(phy = clones[[tree_column]], file = filepath, ...)
}else if(inherits(clones[[tree_column]][[1]], "treedata")){
ape::write.tree(phy = lapply(clones[[tree_column]], function(x)x@phylo),
file = filepath, ...)
}else{
stop(paste(tree_column, "format not recognized."))
}
} else{
stop(paste(tree_column, "not found in the input Dowser object. Please",
"specify what column contains the phylogenetic trees."))
}
}
#' Write the sequences used in tree building to a fasta format. If there are more
#' than one tree in airrClone output the sequence id will be followed by "|clone_id".
#'
#' \code{writeCloneSequences} Exports the sequences used in tree building.
#' @param clones tibble \code{airrClone} objects, the output of
#' \link{formatClones}
#' @param file The file path and name of where the sequences will be saved
#'
#' @export
writeCloneSequences <- function(clones, file){
for(i in 1:nrow(clones)){
clone_id <- clones$clone_id[i]
# grab the germline
if(clones$data[[i]]@phylo_seq == "sequence"){
germline <- clones$data[[i]]@germline
} else if(clones$data[[i]]@phylo_seq == "hlsequence"){
germline <- clones$data[[i]]@hlgermline
} else if(clones$data[[i]]@phylo_seq == "lsequence"){
germline <- clones$data[[i]]@lgermline
}
write(paste0(">Germline|", clone_id), file, append = TRUE)
write(germline, file, append = TRUE)
for(j in 1:nrow(clones$data[[i]]@data)){
seq_id <- clones$data[[i]]@data$sequence_id[j]
if(clones$data[[i]]@phylo_seq == "sequence"){
sequence <- clones$data[[i]]@data$sequence[j]
} else if(clones$data[[i]]@phylo_seq == "hlsequence"){
sequence <- clones$data[[i]]@data$hlsequence[j]
} else if(clones$data[[i]]@phylo_seq == "lsequence"){
sequence <- clones$data[[i]]@data$lsequence[j]
}
write(paste0(">", seq_id, "|", clone_id), file, append = TRUE)
write(sequence, file, append = TRUE)
}
}
}
#'\code{pmlParamsToList}
#' Convert a \code{phangorn::pml} model fit into a flat, JSON-serializable
#' list of fitted model parameters.
#' @param fit object of class \code{pml}, as stored in \code{tree$parameters}
#' after \code{getTrees(..., build="pml")}
#' @noRd
pmlParamsToList <- function(fit){
list(
model = fit$model,
logLik = fit$logLik,
df = fit$df,
k = fit$k,
shape = fit$shape,
inv = fit$inv,
rate = fit$rate,
site.rate = fit$site.rate,
bf = stats::setNames(as.numeric(fit$bf), c("a","c","g","t")),
Q = stats::setNames(as.numeric(fit$Q), c("a_c","a_g","a_t","c_g","c_t","g_t"))
)
}
#'\code{listToPmlParams}
#' Inverse of \code{pmlParamsToList}: rebuild a full, working
#' \code{phangorn::pml} object from a flat parameter list (as reconstructed
#' from a JSON file written by \code{writeTreesJSON}), the clone's real tree,
#' and its real tip sequences.
#' @param lst flat parameter list, as produced from \code{pmlParamsToList}
#' @param tree the clone's reconstructed tree with tip labels matching
#' \code{names(seqs)}
#' @param seqs named character vector of each tip's ungapped
#' sequence (including the germline tip), names matching
#' \code{tree$tip.label}
#' @noRd
listToPmlParams <- function(lst, tree, seqs){
split_seqs <- lapply(seqs, function(s) strsplit(s, "")[[1]])
if(length(lst$bf) == 20){
real_data <- phangorn::phyDat(
ape::as.AAbin(t(as.matrix(dplyr::bind_rows(split_seqs)))),
type="AA")
}else{
real_data <- phangorn::phyDat(
ape::as.DNAbin(t(as.matrix(dplyr::bind_rows(split_seqs)))))
}
fit <- phangorn::pml(
tree = ape::unroot(tree),
data = real_data,
bf = lst$bf,
Q = lst$Q,
inv = lst$inv,
k = lst$k,
shape = lst$shape,
rate = lst$rate,
model = lst$model,
site.rate = lst$site.rate
)
# pml() recomputes logLik from bf/Q, which might be close but not identical
# to the value stored at write time.
fit$logLik <- lst$logLik
fit
}
#'\code{dataFrameToRecords}
#' Convert a data.frame to a list of row-records (one named list per row)
#' for JSON serialization, converting any non-finite numeric value
#' (\code{NaN}, \code{Inf}, \code{-Inf}) to its string form first.
#'
#' \code{jsonlite::write_json(..., auto_unbox=TRUE)} silently drops the key
#' entirely for a data.frame cell holding \code{NaN} when the data.frame is
#' passed to it directly (not rendered as \code{null} -- the field is simply
#' absent from that row's JSON object). BEAST parameter logs routinely
#' contain genuine \code{NaN} values (e.g. \code{geometric.mean} for
#' non-log-scale parameters), so this must be handled explicitly rather than
#' relying on \code{write_json}'s default data.frame conversion.
#' Pre-converting non-finite values to their string form (each field stays a
#' length-1 scalar, so \code{auto_unbox} still unboxes it normally) avoids
#' this; \code{as.numeric()} on read correctly restores
#' \code{NaN}/\code{Inf}/\code{-Inf} from their string forms.
#' @param df a data.frame
#' @noRd
dataFrameToRecords <- function(df){
lapply(seq_len(nrow(df)), function(i){
row <- as.list(df[i, , drop=FALSE])
lapply(row, function(v) if(is.numeric(v) && !is.finite(v)) as.character(v) else v)
})
}
#'\code{writePosteriorTreeList}
#' Store one posterior tree list (\code{tr@info$trees_posterior} or
#' \code{tr@info$trees_with_traits_posterior}) into \code{clone$info}, under
#' field names built from \code{prefix} -- the same raw-treetext + tip-label
#' mechanism \code{\link{writeTimeTreesJSON}} uses for the single summary
#' tree, applied once per posterior sample. Used identically for both
#' posterior tree fields, since they're structurally the same kind of object
#' (a list of \code{treedata} samples) and may be present independently of
#' each other.
#' @param clone_info the in-progress \code{clone$info} list to add to
#' @param posterior_trees a list of \code{treedata} objects (one
#' \code{tr@info$trees_posterior} or
#' \code{tr@info$trees_with_traits_posterior})
#' @param prefix field name prefix, e.g. \code{"trees_posterior"} --
#' produces \code{clone_info[[prefix]]} (treetext list)
#' plus either \code{clone_info[[paste0(prefix,
#' "_tip_labels_shared")]]} or \code{[[paste0(prefix,
#' "_tip_labels")]]}
#' @param ancestor_id \code{"Germline"}
#' @param ancestor_id_qualified the clone-qualified germline tip name
#' @param has_germline does the tree have a germline tip?
#' @noRd
writePosteriorTreeList <- function(clone_info, posterior_trees, prefix,
ancestor_id, ancestor_id_qualified, has_germline){
clone_info[[prefix]] <- lapply(posterior_trees, function(t) t@treetext)
tip_label_list <- lapply(posterior_trees, function(t){
labs <- t@phylo$tip.label
if(has_germline){
labs[labs == ancestor_id] <- ancestor_id_qualified
}
labs
})
# One shared tip-label array for all samples when they share one order
# (the norm -- confirmed on real objects), else one per sample.
shared <- length(tip_label_list) <= 1 ||
all(vapply(tip_label_list[-1], identical, logical(1), tip_label_list[[1]]))
if(shared){
clone_info[[paste0(prefix, "_tip_labels_shared")]] <- tip_label_list[[1]]
}else{
clone_info[[paste0(prefix, "_tip_labels")]] <- tip_label_list
}
clone_info
}
#'\code{dataFrameToValueRows}
#' Like \code{\link{dataFrameToRecords}}, but drops each row's field names,
#' producing a plain JSON array of values per row instead of a JSON object
#' -- for use alongside a column-name vector stored once, separately (the
#' same space-saving idea \code{\link{nexusTreeWithTranslate}} applies to
#' tip names: store the shared "header" once, not repeated per row/tree).
#'
#' Each row is returned as a plain atomic vector when every value in it is
#' numeric and finite: \code{jsonlite::write_json(..., pretty=TRUE)} prints
#' atomic vectors compactly on one line, but a \code{list} of scalars (even
#' all-numeric, unnamed) still gets one element per line. Rows that need
#' string conversion (\code{NaN}/\code{Inf}/\code{-Inf}) -- or, defensively,
#' any row that isn't purely numeric to begin with -- fall back to a
#' \code{list}, so mixed element types are never silently coerced (an atomic
#' vector with any non-numeric value would force every element in that row
#' to character).
#' @param df a data.frame, in the fixed column order the row values
#' should be emitted in
#' @noRd
dataFrameToValueRows <- function(df){
lapply(seq_len(nrow(df)), function(i){
row <- unname(as.list(df[i, , drop=FALSE]))
if(all(vapply(row, is.numeric, logical(1))) &&
all(vapply(row, is.finite, logical(1)))){
unlist(row)
}else{
lapply(row, function(v) if(is.numeric(v) && !is.finite(v)) as.character(v) else v)
}
})
}
#'\code{writeTreesJSON}
#' Experimental. Write trees in AIRR format
#' @param object Dowser object from getTrees
#' @param file name of .json file
#' @param repertoire_id repertoire_id to use
#' @param check verify object is equivalent on reloading
#' @param verbose print out more info
#' @param edge_tol tolerance for branch length checks (if check=TRUE)
#' @param cell cell id column name in Dowser object
#' @param heavy name of heavy chain locus
#' @param light names of light chain loci
#' @param dowser_fields include dowser-specific information? (recommended)
#' @param nproc number of cores to use (parallelizes by clone)
#' @details
#' Works with trees built by any of \code{getTrees}'s \code{build} options
#' (\code{"pratchet"}, \code{"pml"}, \code{"igphyml"}, \code{"raxml"}).
#' \code{getTrees(..., build="pml")} trees store the full
#' \code{phangorn::optim.pml} fit in \code{tree$parameters}; since that object
#' isn't JSON-serializable (and isn't meaningful to reconstruct from a file),
#' it's reduced to a flat list of fitted model parameters via
#' \code{pmlParamsToList} before being written out. See that function's
#' documentation for what is kept and why.
#' @export
writeTreesJSON = function(object, file, repertoire_id="sample", check=TRUE, verbose=TRUE,
edge_tol=1e-8, cell="cell_id", heavy="IGH", light=c("IGK","IGL"), dowser_fields=TRUE,
nproc=1){
if(verbose){
print("Note: internal node numbers/labels not currently preserved.")
}
if(inherits(object$trees[[1]], "treedata")){
timetree <- TRUE
}else{
timetree <- FALSE
}
clones <- list()
clones$Clone <- list()
clones$Rearrangement <- list()
rearrangement_index <- 1
for(row in 1:nrow(object)){
# AIRR standard fields for clone
clone <- list()
clone$clone_id <- object$clone_id[row]
clone$repertoire_id <- repertoire_id
clone$repertoire_group_id <- NA
clone$data_processing_id <- NA
clone$clone_count <- object$seqs[row]
clone$sequences <- object$data[[row]]@data$sequence_id
if(dowser_fields){
# nonstandard but important for dowser
clone$info <- list()
if(timetree){
clone$program_origin <- "dowser-timetree"
}else{
clone$program_origin <- "dowser-phylo"
}
clone$program_version <- as.character(packageVersion("dowser"))
#clone$info$locus <- object$data[[row]]@locus
clone$info$region <- object$data[[row]]@region
clone$info$numbers <- object$data[[row]]@numbers
clone$info$phylo_seq <- object$data[[row]]@phylo_seq
clone$v_call <- object$data[[row]]@v_gene
clone$j_call <- object$data[[row]]@j_gene
clone$junction_length <- object$data[[row]]@junc_len
clone$column_names <- names(object)
}
node_class <- "Cell"
node_index <- "cell_id"
if(!cell %in% names(object$data[[row]]@data)){
node_class <- "Rearrangement"
node_index <- "sequence_id"
if(object$data[[row]]@phylo_seq == "hlsequence"){
stop(paste(cell," not found in data object but paired chain data used"))
}
}
clone$clone_class <- node_class
germline_name <- "Germline"
germline_qualified <- paste0(germline_name, "-", clone$clone_id)
if(timetree){
tr <- object$trees[[row]]
phy <- tr@phylo
}else{
phy <- object$trees[[row]]
}
nodes <- length(phy$tip.label) + phy$Nnode
phy$node.label <- paste0("Node",(length(phy$tip.label)+1):nodes,"-",clone$clone_id)
phy$tip.label[phy$tip.label == germline_name] = germline_qualified
airrc <- object$data[[row]]
germ_seq <- switch(airrc@phylo_seq,
"sequence" = airrc@germline,
"lsequence" = airrc@lgermline,
"hlsequence" = airrc@hlgermline)
# add info from other columns
forbidden <- c("region", "numbers", "phylo_seq", "germline",
"lgermline", "hlgermline","program_origin", "program_version")
for(n in names(object)){
if(n %in% c("clone_id","data","locus","seqs","trees")){
next
}
if(n %in% forbidden){
stop(paste("Forbidden column names:",paste(forbidden, collapse=",")))
}
if(dowser_fields){clone$info[[n]] <- object[[n]][[row]]}
}
object$data[[row]]@data$tip_order <- 1:nrow(object$data[[row]]@data)
tipcols <- names(object$data[[row]]@data)
tipdata <- list()
for(i in 1:nrow(object$data[[row]]@data)){
tip <- list()
for(col in tipcols){
if(col %in% c("sequence","lsequence","hlsequence")){
next
}
tip[[col]] <- object$data[[row]]@data[i,][[col]]
}
tipdata[[tip$sequence_id]] <- tip
}
clone$tree <- ape::write.tree(phy)
if(dowser_fields){
clone$info$treeinfo <- list()
for(n in names(phy)){
if(n %in% c("edge","tip.label","Nnode",
"edge.length","nodes","node.label", "state")){
next
}
value <- phy[[n]]
# phangorn::pml fits (getTrees(..., build="pml")) aren't
# JSON-serializable as-is
if(n == "parameters" && inherits(value, "pml")){
value <- pmlParamsToList(value)
}
clone$info$treeinfo[[n]] <- value
}
}
ucanode <- ape::getMRCA(phy, tip=phy$tip.label)
if(sum(phy$tip.label == germline_qualified) > 0){
germline_node <- which(phy$tip.label == germline_qualified)
}else{
germline_node <- NA
clone$inferred_ancestor <- NA
}
# metadata columns for tips
nodes <- list()
for(i in 1:(length(phy$tip.label) + phy$Nnode)){
node <- list()
if(dowser_fields){
node$info <- list()
}
node$node_class <- node_class
node$repertoire_id <- repertoire_id
receptors <- list()
if(i <= length(phy$tip.label) && (is.na(germline_node) | i != germline_node)){
node$node_type <- "observed"
if(dowser_fields){
node$info$tipdata <- tipdata[[phy$tip.label[i]]]
}
seqid <- phy$tip.label[i]
node$node_id <- seqid
if(node_class == "Cell"){
node$cell_id <- object$data[[row]]@data[
object$data[[row]]@data$sequence_id == seqid,][[cell]]
}else{
node$sequence_id <- seqid
}
}else{
node$node_type <- "inferred"
if(!is.na(germline_node) && i == germline_node){
node[[node_index]] <- phy$tip.label[i]
}else{
node[[node_index]] <- paste0(phy$node.label[i-length(phy$tip.label)])
}
node$node_id <- node[[node_index]]
}
nodenames <- names(phy$nodes[[i]])
for(n in nodenames){
if(n %in% c("id", "sequence")){
next
}
if(dowser_fields){
node$info[[n]] <- unlist(phy$nodes[[i]][n])
}
}
node$number <- i
node$parent <- phy$edge[phy$edge[,2] == i,1]
node$children <- phy$edge[phy$edge[,1] == i,2]
seqa <- getNodeSeq(object, node=i, tree=phy, gaps=TRUE)
seq <- getNodeSeq(object, node=i, tree=phy, gaps=FALSE)
if(!is.na(germline_node)){
ga <- getNodeSeq(object, node=germline_node, tree=phy, gaps=TRUE)
}else{
ga <- NA
}
for(loci_i in 1:length(seq)){
receptor <- list()
loci <- names(seq)[loci_i]
addon <- ""
if(node_class == "Cell"){
receptor$cell_id <- node$cell_id
addon <- paste0("-",loci)
}
receptor$sequence_id <- paste0(node[[node_index]], addon)
receptor$sequence_alignment <- seqa[loci_i]
receptor$sequence <- seq[loci_i]
receptor$germline_alignment <- ga[loci_i]
receptor$locus <- loci
receptor$rev_comp <- NA
receptor$productive <- NA
receptor$v_call <- NA
receptor$d_call <- NA
receptor$j_call <- NA
receptor$junction <- NA
receptor$junction_aa <- NA
receptor$junction_length <- NA
receptor$v_cigar <- NA
receptor$d_cigar <- NA
receptor$j_cigar <- NA
receptor$rearrangement_type <- node$node_type
clones$Rearrangement[[rearrangement_index]] <- receptor
rearrangement_index <- rearrangement_index + 1
}
if(!is.null(phy$state)){
node$state_vector_value <- phy$state[i]
}
#node$node_id <- node[[node_index]]
nodes[[i]] <- node
if(!is.na(germline_node) && node$number == germline_node){
clone$inferred_ancestor <- node$node_id
}
}
clone$nodes <- nodes
if(timetree){
clone$info$treetext <- tr@treetext
main_tip_labels <- phy$tip.label
if(!is.na(germline_node)){
main_tip_labels[main_tip_labels == germline_name] <- germline_qualified
}
clone$info$tip_labels <- main_tip_labels
#clone$info$root_edge <- tr@phylo$root.edge
clone$info$beast_parameters <- dataFrameToRecords(tr@info$parameters)
# Full posterior (full_posterior=TRUE), when present -- see this
# function's @details. tr@info$trees_posterior (the raw sampled trees)
# and tr@info$trees_with_traits_posterior (a separate, typically
# thinned, set of posterior trees carrying ancestral trait
# reconstruction in @data) are two independent, optional fields --
# any combination of them (and parameters_posterior) may be present.
# Each is stored the same way, via writePosteriorTreeList().
for(prefix in c("trees_posterior", "trees_with_traits_posterior")){
posterior_trees <- tr@info[[prefix]]
if(!is.null(posterior_trees)){
clone$info <- writePosteriorTreeList(clone$info, posterior_trees,
prefix, germline_name, germline_qualified, !is.na(germline_node))
}
}
if(!is.null(tr@info$parameters_posterior)){
#wide_pp <- tidyr::pivot_wider(tr@info$parameters_posterior,
# names_from="parameter", values_from="value")
wide_pp <- tr@info$parameters_posterior
clone$info$parameters_posterior_columns <- names(wide_pp)
clone$info$parameters_posterior <- dataFrameToValueRows(wide_pp)
}
}
clones$Clone[[row]] <- clone
}
if(grepl(".json$",file)){
# digits=NA keeps small floating points from getting truncated
jsonlite::write_json(clones, file, pretty=TRUE, simplifyVector=TRUE,
auto_unbox = TRUE, digits=NA)
}else{
stop("file extension not recognized, must end in .json")
}
# verify object was stored faithfully
if(check){
if(verbose){
print("Loading object to check consistency")
}
nobject <- readTreesJSON(file, heavy=heavy, light=light, verbose=verbose, nproc=nproc)
# dowserObjectEquivalent understands pml fits directly (reducing them via
# pmlParamsToList itself), so no pre-normalization is needed here.
validate <- dowserObjectEquivalent(object, nobject, verbose=verbose,
edge_tol=edge_tol, dowser_fields=dowser_fields, nproc=nproc)
}
}
#' \code{nexusTreeWithTranslate}
#' Wrap a raw, numeric-tip-coded BEAST/NEXUS treetext string in a minimal
#' NEXUS \code{Translate} block, so it can be parsed with
#' \code{treeio::read.beast()}.
#' @details
#' \code{treetext} (\code{tr@treetext} for a \code{treeio::treedata} object)
#' is the raw annotated tree string with numeric tip codes (e.g. \code{"1"},
#' \code{"2"}, ...), not real tip names -- resolving those codes to real
#' names requires the NEXUS \code{Translate} block that originally
#' accompanied it, which maps code \code{i} to the i-th listed taxon. That
#' mapping is exactly \code{tr@phylo$tip.label} in \code{phylo}'s own tip
#' order (tip number \code{i} in a NEXUS-translate-parsed tree is always the
#' \code{i}-th translate-table entry, by construction), so re-wrapping
#' \code{treetext} with a translate block built from that same array and
#' parsing with \code{treeio::read.beast()} reconstructs \code{@phylo}/
#' \code{@data} exactly as \code{getTimeTrees()}/\code{readBEAST()} do.
#'
#' \code{treeio::read.beast.newick()} on the bare \code{treetext} (no
#' translate block) is NOT a safe substitute: it assigns tip numbers by
#' first-occurrence order in its own parse of the tree string, a completely
#' different and unrelated convention to the translate table's listed
#' order. The two conventions can agree by coincidence for one tree shape
#' and silently diverge for another (or after a Newick string gets
#' re-serialized with a different node rotation, e.g. across repeated
#' write/read cycles) -- producing tip labels silently attached to the
#' wrong node rather than an error.
#' @param treetext raw NEXUS-numeric-coded tree string (\code{tr@treetext})
#' @param tip_labels real tip names, in numeric-code order (i.e.
#' \code{tip_labels[i]} is the name for code \code{i})
#' @noRd
nexusTreeWithTranslate <- function(treetext, tip_labels){
translate_lines <- paste0(seq_along(tip_labels), " ", tip_labels)
translate_lines[-length(translate_lines)] <- paste0(translate_lines[-length(translate_lines)], ",")
paste(c(
"#NEXUS",
"Begin trees;",
"Translate",
translate_lines,
";",
paste0("tree TREE1 = ", treetext),
"End;"
), collapse="\n")
}
#' \code{readTreesJSON}
#' Experimental. Read trees from JSON/AIRR format from Dowser
#' @param file .json file
#' @param heavy name of heavy chain locus
#' @param light names of light chain loci
#' @param verbose how much info to print
#' @param edge_tol tolerance for branch length checks (if check=TRUE)
#' @param nproc number of cores to use (parallelizes by clone)
#' @details
#' Reads files written by \code{\link{writeTreesJSON}}, including trees built
#' with any \code{getTrees} \code{build} option.
#' @export
readTreesJSON = function(file, heavy="IGH", light=c("IGK","IGL"),
verbose=TRUE, edge_tol=1e-8, nproc=1){
rclones <- jsonlite::read_json(file)
program_origin <- rclones$Clone[[1]]$program_origin
output <- dplyr::tibble()
outtrees <- list()
outdata <- list()
rearrangements <- rclones$Rearrangement
if("program_origin" %in% names(rclones$Clone[[1]]) &&
rclones$Clone[[1]]$program_origin == "dowser-phylo"){
dowser <- TRUE
timetree <- FALSE
}else if("program_origin" %in% names(rclones$Clone[[1]]) &&
rclones$Clone[[1]]$program_origin == "dowser-timetree"){
dowser <- TRUE
timetree <- TRUE
}else{
if(verbose)warning("Some features limited by using non-Dowser JSON")
dowser <- FALSE
timetree <- FALSE
}
node_class <- rclones$Clone[[1]]$clone_class
if(node_class == "Cell"){
node_index <- "cell_id"
}else{
node_index <- "sequence_id"
}
rearrangement_names <- sapply(rearrangements, function(x)x[[node_index]])
for(ci in 1:length(rclones$Clone)){
clone <- rclones$Clone[[ci]]
seqs <- clone$clone_count[[1]]
rphy <- ape::read.tree(text=clone$tree)
nodes <- clone$nodes
loci <- c()
numbers <- c()
column_names <- c()
ancestor_node_id <- ifelse(is.null(clone$inferred_ancestor),
NA,clone$inferred_ancestor)
rnodes <- list()
tips <- list()
tipcount <- 1
for(i in 1:length(nodes)){
node <- nodes[[i]]
#node_num = node$info$number[[1]]
if(node$node_id %in% rphy$tip.label){
new_num <- which(rphy$tip.label == node$node_id)
}else if(node$node_id %in% rphy$node.label){
new_num <- which(rphy$node.label == node$node_id) + length(rphy$tip.label)
}else{
stop(paste(node$node_id, "not found in tree"))
}
new_node <- list()
for(name in names(node$info)){
if(name %in% c("number","parent","children","tipdata")){
next
}else if(name == "state_vector_value"){
if(is.null(rphy$state)){
rphy$state <- rep(NA, length=length(nodes))
}
rphy$state[new_num] <- unlist(node$info[[name]])
}else{
new_node[[name]] <- node$info[[name]]
}
}
receptors <- rearrangements[rearrangement_names == node[[node_index]]]
receptor_loci <- sapply(receptors, function(x)x$locus)
loci_order <- order(receptor_loci)
if(node_class == "Cell" & receptor_loci[loci_order[1]] != heavy){
stop(paste(heavy, " not in first ordered locus position"))
}
# assumes reeptors are in same order as locus object
seq <- ""
for(lo in loci_order){
seq <- paste0(seq, receptors[[lo]]$sequence)
if(i == 1){
loci <- c(loci, rep(receptors[[lo]]$locus,
nchar(receptors[[lo]]$sequence)))
if(!dowser){
numbers <- c(numbers, 1:nchar(receptors[[lo]]$sequence))
}
}
}
if(i == 1){
unique_loci <- unique(loci)
if(dowser && !is.null(clone$info$phylo_seq)){
# Dowser-written files record the true phylo_seq; use it
# directly instead of re-deriving it from the locus tag
phylo_seq <- clone$info$phylo_seq[[1]]
column_names <- unlist(clone$column_names)
}else if(heavy %in% loci && length(intersect(light,unique_loci)) > 0){
phylo_seq <- "hlsequence"
}else if(unique_loci[1] == heavy && length(unique_loci) == 1){
phylo_seq <- "sequence"
}else if(length(intersect(light,unique_loci)) > 0){
phylo_seq <- "lsequence"
}else{
warning(paste("Could assign phylo_seq based on locus column, using 'sequence'",
paste(unique_loci, collapse=",")))
phylo_seq <- "sequence"
}
}
if(nchar(seq) != length(loci)){
if(!timetree)stop("seq and loci vector not the same length!")
}
if(seq == ""){seq <- NA}
new_node$sequence <- seq
node$sequence <- seq
rnodes[[new_num]] <- new_node
if(node$node_id %in% rphy$tip.label){
tips[[tipcount]] <- node
tipcount <- tipcount + 1
}
}
rphy$nodes <- rnodes
rphy$node.label <- NULL
rphy$name <- clone$clone_id
if(!dowser){
rphy <- setNodeDivergences(rphy)
}
for(n in names(clone$info$treeinfo)){
if(n == "state"){
next
}
ni <- clone$info$treeinfo[[n]]
if(length(ni) > 1){
ni <- lapply(ni, function(x)unlist(x))
if(n == "parameters" && !is.null(clone$info$treeinfo$tree_method) &&
grepl("optim\\.pml", clone$info$treeinfo$tree_method)){
tip_seqs <- stats::setNames(
sapply(tips, function(tp) tp$sequence),
sapply(tips, function(tp) tp$node_id))
tree_for_pml <- rphy
# if this was a scaled tree, convert back to genetic distance for PML object
if(dowser && clone$info$treeinfo$edge_type == "mutations"){
tree_for_pml$edge.length <- tree_for_pml$edge.length/length(loci)
}
#only mess with the germline name if coming from Dowser
if(dowser && !is.na(ancestor_node_id)){
names(tip_seqs)[names(tip_seqs) == ancestor_node_id] <- "Germline"
tree_for_pml$tip.label[tree_for_pml$tip.label == ancestor_node_id] <- "Germline"
}
ni <- listToPmlParams(ni, tree=tree_for_pml, seqs=tip_seqs)
}
rphy[[n]] <- ni
}else{
rphy[[n]] <- ni[[1]]
}
}
#extract data from the tips
data <- list()
germline <- NA
for(i in 1:length(tips)){
tip <- tips[[i]]
if(dowser){
row <- tip$info$tipdata
}else{
row <- list()
row$sequence_id <- tip$node_id
}
row[[phylo_seq]]<- tip$sequence
if(is.na(ancestor_node_id) || tip$node_id != ancestor_node_id){
data[[i]] <- row
}else{
germline <- row[[phylo_seq]]
}
}
bdata <- dplyr::bind_rows(data)
alignment_width <- unique(nchar(bdata[[phylo_seq]]))
if(length(alignment_width) > 1){
stop(paste(rclones$Clone[[ci]]$clone_id, "data sequences different length"))
}
if(is.na(germline)){
germline <- paste(rep("N",times=alignment_width), collapse="")
}
if(dowser){
bdata <- bdata[order(bdata$tip_order),]
bdata <- dplyr::select(bdata, -"tip_order")
if(!is.na(ancestor_node_id)){
rphy$tip.label[rphy$tip.label == ancestor_node_id] <- "Germline"
}
}
lgermline <- ""
hlgermline <- ""
if(!dowser){
region <- rep("N",times=alignment_width)
}
if(phylo_seq == "hlsequence"){
hlgermline <- germline
lgermline <- ""
germline <- ""
if(!dowser){
region <- rep("N",times=alignment_width)
}
}else if(phylo_seq == "lsequence"){
lgermline <- germline
germline <- ""
hlgermline <- ""
if(!dowser){
region <- rep("N",times=alignment_width)
}
}
if(dowser){
region <- unlist(clone$info$region)
numbers <- unlist(clone$info$numbers)
}
outclone <- new("airrClone",
data=bdata,
clone=as.character(clone$clone_id[[1]]),
germline=germline,
lgermline=lgermline,
hlgermline=hlgermline,
v_gene=ifelse("v_call" %in% names(clone),unlist(clone$v_call),""),
j_gene=ifelse("j_call" %in% names(clone),unlist(clone$j_call),""),
junc_len=ifelse("junction_length" %in% names(clone),clone$junction_length[[1]],0),
locus=loci,
region=region,
numbers=numbers,
phylo_seq=phylo_seq)
outdata[[ci]] <- outclone
if(!timetree){
outtrees[[ci]] <- rphy
}else{
# specialized functions for reading in treedata objects
# See nexusTreeWithTranslate, required for tip identity to
# come out correctly
tmpfile <- tempfile(fileext=".tree")
writeLines(nexusTreeWithTranslate(clone$info$treetext[[1]],
unlist(clone$info[["tip_labels"]])), tmpfile)
tr <- treeio::read.beast(tmpfile)
unlink(tmpfile)
if(!is.na(ancestor_node_id)){
tr@phylo$tip.label[tr@phylo$tip.label == ancestor_node_id] <- "Germline"
}
# check and copy over attributes that were read in from the nodes list
# this might seem redundant but ensures that two versions of the same
# tree are consistent.
# node_map, rphy node = node_map[tr@phylo node]
if(!treesEquivalent(rphy, tr@phylo, edge_tol=edge_tol)){
stop(paste(clone$clone_id, "tree and treetext trees not the same"))
}
node_map <- mapSubtrees(tr@phylo, rphy)
tr@phylo$nodes <- lapply(1:length(rphy$nodes), function(x)rphy$nodes[[node_map[x]]])
for(n in names(rphy)){
if(n %in% c("edge", "edge.length", "Nnode", "tip.label", "nodes")){
next
}else{
tr@phylo[[n]] <- rphy[[n]]
}
}
#tr@phylo$root.edge <- if(is.null(clone$info$root_edge)) 0 else clone$info$root_edge[[1]]
# read in beast parameter estimates
beast_params <- clone$info$beast_parameters
if(!is.null(beast_params)){
params_df <- dplyr::bind_rows(lapply(beast_params, function(row){
as.data.frame(lapply(row, function(x) as.character(x[[1]])), stringsAsFactors=FALSE)
}))
# coerce every column but "item" back to numeric
for(col in names(params_df)){
if(col != "item"){
params_df[[col]] <- suppressWarnings(as.numeric(params_df[[col]]))
}
}
tr@info$parameters <- params_df
}
# read in tree posterior fields
for(prefix in c("trees_posterior", "trees_with_traits_posterior")){
if(prefix %in% names(clone$info)){
tips_shared <- clone$info[[paste0(prefix, "_tip_labels_shared")]]
tips_raw <- clone$info[[paste0(prefix, "_tip_labels")]]
raw <- clone$info[[prefix]]
tr@info[[prefix]] <- parallel::mclapply(seq_along(raw), function(k){
tip_labels_k <- if(!is.null(tips_shared)){
unlist(tips_shared)
}else{
unlist(tips_raw[[k]])
}
tmpfile_k <- tempfile(fileext=".tree")
writeLines(nexusTreeWithTranslate(raw[[k]], tip_labels_k), tmpfile_k)
pt <- treeio::read.beast(tmpfile_k)
unlink(tmpfile_k)
if(!is.na(ancestor_node_id)){
pt@phylo$tip.label[pt@phylo$tip.label == ancestor_node_id] <- "Germline"
}
pt
}, mc.cores=nproc)
}
}
# read in parameters posterior
# [[ ]], not $: consistent with the posterior-tree fields above.
params_post_cols <- clone$info[["parameters_posterior_columns"]]
params_post_raw <- clone$info[["parameters_posterior"]]
if(!is.null(params_post_raw)){
cols <- unlist(params_post_cols)
params_post_wide <- dplyr::bind_rows(lapply(params_post_raw, function(row){
vals <- vapply(row, as.character, character(1))
as.data.frame(as.list(stats::setNames(vals, cols)), stringsAsFactors=FALSE)
}))
for(col in names(params_post_wide)){
params_post_wide[[col]] <- suppressWarnings(as.numeric(params_post_wide[[col]]))
}
tr@info$parameters_posterior <- params_post_wide
}
outtrees[[ci]] <- tr
}
# store info in other columns
temp <- dplyr::tibble(clone_id=clone$clone_id[[1]], seqs=nrow(outclone@data),
locus=paste0(sort(unique(loci)),collapse=","))
for(n in names(clone$info)){
if(!n %in% c("region", "numbers", "phylo_seq", "germline", "lgermline",
"hlgermline", "trees", "data", "clone_id", "seqs", "locus", "program_origin", "treeinfo",
"program_version", "column_names",
"treetext", "tip_labels", "root_edge", "beast_parameters",
"trees_posterior", "trees_posterior_tip_labels", "trees_posterior_tip_labels_shared",
"trees_with_traits_posterior", "trees_with_traits_posterior_tip_labels",
"trees_with_traits_posterior_tip_labels_shared",
"parameters_posterior", "parameters_posterior_columns")){
#print(n)
ni <- clone$info[[n]]
if(timetree && n %in% c("parameters","skyline")){
ni <- as.data.frame(dplyr::bind_rows(ni))
temp[[n]] <- list(ni)
}else if(length(ni) > 1){
ni <- lapply(ni, function(x)unlist(x))
temp[[n]] <- list(ni)
}else{
temp[[n]] <- ni[[1]]
}
}
}
output <- dplyr::bind_rows(output, temp)
}
output$data <- outdata
output$trees <- outtrees
onames <- names(output)
if(dowser){
if(length(setdiff(onames, column_names)) > 0){
print(paste(onames))
print(paste(column_names))
stop("mismatch in object column names")
}
output <- output[,column_names]
}else{
standard <- c("clone_id", "data", "seqs", "locus", "trees")
nonstandard <- setdiff(onames, standard)
output <- dplyr::select(output, dplyr::any_of(standard), dplyr::any_of(nonstandard))
}
output
}
#'\code{pmlParamsEqual}
#' Compare two sets of \code{phangorn::pml} fit parameters for equality,
#' within a numeric tolerance.
#'
#' Accepts either a full \code{pml} fit object (as stored in \code{tree$parameters}
#' after \code{getTrees(..., build="pml")}) or the flat list produced by
#' \code{\link{pmlParamsToList}} (as stored after a JSON round-trip) on either
#' side. Also works with RAxML and IgPhyML parameter lists
#'
#' When \emph{both} sides are full \code{pml} objects with a real
#' \code{$data}/\code{$tree} (i.e. both went through
#' \code{\link{listToPmlParams}}'s reconstruction -- \code{igphyml}/
#' \code{raxml} parameter lists never have these), the real alignment and
#' real tree are also compared: alignment content via \code{as.character()}
#' on the \code{phyDat} (tip order-independent, matched by name), and the
#' tree via pairwise tip-to-tip (patristic) distances from
#' \code{ape::cophenetic.phylo()} rather than a direct edge-by-edge
#' comparison -- topologically identical trees can have their internal nodes
#' numbered/ordered differently (e.g. after an \code{ape::unroot()}), and
#' cophenetic distances are invariant to that while still fully capturing
#' both topology and branch lengths.
#' @param pa first \code{parameters} value (\code{pml} object or plain list)
#' @param pb second \code{parameters} value (\code{pml} object or plain list)
#' @param tol relative tolerance (passed to \code{all.equal}) for comparing
#' fitted scalar/vector values
#' @param edge_tol absolute tolerance for comparing the real tree's pairwise tip
#' distances, when both sides have a tree
#' @noRd
pmlParamsEqual <- function(pa, pb, tol=1e-3, edge_tol=1e-8){
pa_full <- pa
pb_full <- pb
if(inherits(pa, "pml")){ pa <- pmlParamsToList(pa) }
if(inherits(pb, "pml")){ pb <- pmlParamsToList(pb) }
if(!setequal(names(pa), names(pb))){
return(FALSE)
}
for(nm in names(pa)){
# unname(): a named vector (e.g. pmlParamsToList's labeled bf/Q) survives
# a JSON round-trip as a plain unnamed array, so names must be ignored
# here or a real value match still reports as unequal.
#va <- uname(unlist(pa[[nm]]))
#vb <- uname(unlist(pb[[nm]]))
va <- unlist(pa[[nm]])
vb <- unlist(pb[[nm]])
if(length(va) != length(vb)){
return(FALSE)
}
if(is.numeric(va) && is.numeric(vb)){
if(!isTRUE(all.equal(va, vb, tolerance=tol))){
return(FALSE)
}
}else if(any(as.character(va) != as.character(vb))){
return(FALSE)
}
}
# Real alignment, when both sides have one (only true full pml fits do --
# igphyml/raxml parameter lists never carry data/tree).
# [[ ]], not $ avoid partial tree and tree_length match with raxml/igphyml
data_a <- pa_full[["data"]]
data_b <- pb_full[["data"]]
if(is.null(data_a) != is.null(data_b)){
return(FALSE)
}
if(!is.null(data_a)){
da <- as.character(data_a)
db <- as.character(data_b)
if(!setequal(rownames(da), rownames(db))){
return(FALSE)
}
db <- db[rownames(da), , drop=FALSE]
if(!identical(dim(da), dim(db)) || sum(da != db) != 0){
return(FALSE)
}
}
tree_a <- pa_full[["tree"]]
tree_b <- pb_full[["tree"]]
if(is.null(tree_a) != is.null(tree_b)){
return(FALSE)
}
if(!is.null(tree_a)){
if(!setequal(tree_a$tip.label, tree_b$tip.label)){
return(FALSE)
}
ca <- ape::cophenetic.phylo(tree_a)
cb <- ape::cophenetic.phylo(tree_b)[rownames(ca), rownames(ca)]
if(!isTRUE(all.equal(ca, cb, tolerance=edge_tol, check.attributes=FALSE))){
return(FALSE)
}
}
return(TRUE)
}
#' Get a map of nodes from one
#' @param treea First phylo object
#' @param treeb Second phylo object
#' @details
#' Returns a vector in which each position is the numerical
#' node number in treea, and the value is the corresponding node number in treeb
#' So node 1 in treea corresponds to result[1] in treeb
#' @export
mapSubtrees = function(treea, treeb){
nodesb <- c() #map of nodes from a to b
tipsa <- treea$tip.label
tipsb <- treeb$tip.label
if(!setequal(tipsa, tipsb)){
diffs <- c(setdiff(tipsb, tipsa), setdiff(tipsa, tipsb))
fdiffs <- diffs[!grepl("Germline", diffs)]
if(length(fdiffs) == 0){
treea$tip.label[treea$tip.label %in% diffs] = "Germline"
treeb$tip.label[treeb$tip.label %in% diffs] = "Germline"
}else{
stop(paste("tips not the same"))
}
}
all_subtrees_a <- lapply(1:(length(treea$tip.label) + treea$Nnode), function(x)getSubTaxa(x, treea))
all_subtrees_b <- lapply(1:(length(treeb$tip.label) + treeb$Nnode), function(x)getSubTaxa(x, treeb))
if(length(all_subtrees_a) != length(all_subtrees_b)){
stop(paste("trees don't have the same number of subtrees"))
}
# check if trees have the same subtrees with corresponding edge lengths and sequences
for(sta in 1:length(all_subtrees_a)){
sa <- all_subtrees_a[[sta]]
match <- -1
for(stb in 1:length(all_subtrees_b)){
if(setequal(sa, all_subtrees_b[[stb]])){
match <- stb
}
}
if(match < 0){
stop(paste("subtrees don't match"))
treecheck = FALSE
}
nodesb[sta] <- match
}
nodesb
}
#'\code{dataColumnEqual}
#' Compare one \code{@data} column between two BEAST-annotated trees,
#' tolerant of a floating-point string-formatting artifact: BEAST numeric
#' annotations are stored as \emph{character} strings (not \code{numeric}),
#' and \code{treeio::read.beast()} is not idempotent on them -- re-parsing a
#' tree's own \code{@treetext} a second time can shift the last
#' significant digit of a numeric annotation's string representation (e.g.
#' \code{"0.51802396741998"} vs \code{"0.518023967419979"} for the exact
#' same underlying value), confirmed by re-parsing a real object's own
#' \code{@treetext} independently of any write/read round trip. Columns
#' that parse fully as numeric are compared with tolerance; anything else
#' (e.g. categorical trait columns like \code{location}) is compared
#' exactly, as before.
#' @param va, vb the two columns to compare
#' @param tolerance numeric tolerance for columns that parse as numeric
#' @noRd
dataColumnEqual <- function(va, vb){
if(is.character(va) && is.character(vb)){
na_num <- suppressWarnings(as.numeric(va))
nb_num <- suppressWarnings(as.numeric(vb))
if(!anyNA(na_num) && !anyNA(nb_num)){
return(isTRUE(all.equal(na_num, nb_num, check.attributes=FALSE)))
}else{
# KBH
# if same indexes are NA, compare values that aren't
if(sum(is.na(na_num) != is.na(nb_num)) == 0){
na_index <- !is.na(na_num)
return(isTRUE(all.equal(na_num[na_index], nb_num[na_index],
check.attributes=FALSE)))
}else{
return(FALSE)
}
}
}
isTRUE(all.equal(va, vb))
}
#' Check whether two tree objects are equivalent
#' @param obja First phylo or treedata object
#' @param objb Second phylo or treedata object
#' @param edge_tol tolerance for branch length checks (if check=TRUE)
#' @param numbering_match require internal node numbers to match?
#' @param clonesa Dowser clones object associated with obja (if check_extended=TRUE)
#' @param clonesb Dowser clones object associated with objb (if check_extended=TRUE)
#' @param check_extended Also check node sequences and state vector? Requires clonesa/b to be supplied
#' @param gaps check sequences wth IMGT gaps with check_extended?
#' @details For treedata objects, check both @phylo and @data
#' @export
treesEquivalent = function(obja, objb, edge_tol=1e-8, numbering_match=FALSE,
clonesa=NULL, clonesb=NULL, check_extended=FALSE, gaps=TRUE){
treecheck <- TRUE
if(check_extended){
if(is.null(clonesa) || is.null(clonesb)){
stop("clonesa and clonesb must be provided if check_extended=TRUE")
}
}
a_is_timetree <- inherits(obja, "treedata")
b_is_timetree <- inherits(objb, "treedata")
if(a_is_timetree != b_is_timetree){
stop("Cannot compare a time-tree object against a non-time-tree object")
}
if(a_is_timetree){
timetree <- TRUE
treea <- obja@phylo
treeb <- objb@phylo
}else{
timetree <- FALSE
treea <- obja
treeb <- objb
}
node_map <- tryCatch(mapSubtrees(treea, treeb), error=function(e)e)
if(inherits(node_map, "error")){
warning(node_map)
return(FALSE)
}
# check if trees have the same subtrees with corresponding edge lengths and sequences
for(nodea in 1:length(node_map)){
nodeb <- node_map[nodea]
ea <- round(treea$edge.length[treea$edge[,2] == nodea], digits=11)
eb <- round(treeb$edge.length[treeb$edge[,2] == nodeb], digits=11)
if(length(ea) > 0 || length(eb) > 0){
if(abs(ea - eb) > edge_tol){
warning(paste( "Edges not within edge_tol", ea, eb, nodea, nodeb))
treecheck = FALSE
}
}
if(check_extended){
seqa <- getNodeSeq(clonesa, node=nodea, tree=treea, gaps=gaps)
seqb <- getNodeSeq(clonesb, node=nodeb, tree=treeb, gaps=gaps)
if(!is.null(seqa) && !is.null(seqb)){
treecheck = isTRUE(all.equal(seqa, seqb))
if(!treecheck){
warning(paste("sequences not identical", nodea, nodeb))
}
}
if(!is.null(treea$state) || !is.null(treeb$state)){
if(treea$state[nodea] != treeb$state[nodeb]){
warning(paste("node states not equal",treea$state[nodea],
treea$state[nodeb], nodea, nodeb))
}
}
}
}
if(timetree){
da <- obja@data
db <- objb@data
# node numbers aren't necessarily the same, so map one to the other
da$nodeb <- as.character(node_map[as.numeric(da$node)])
da <- da[order(da$nodeb),]
db <- db[order(db$node),]
if(!identical(da$node, db$node) && numbering_match){
warning(paste("@data node numbering not the same"))
treecheck <- FALSE
}
#da <- select(da, -!!rlang::sym("nodeb"))
da <- da[,names(da) != "nodeb"]
if(!setequal(names(da), names(db))){
warning(paste("@data columns not the same"))
treecheck <- FALSE
}
for(col in names(da)){
if(col == "node"){
next
}
if(!dataColumnEqual(da[[col]], db[[col]])){
warning(paste("@data column", col, "not the same"))
treecheck <- FALSE
}
}
}
treecheck
}
#'\code{dowserObjectEquivalent}
#' Experimental. Check if two Dowser objects are equivalent
#' @param obj1 First Dowser object
#' @param obj2 Second Dowser object
#' @param verbose print out more info
#' @param edge_tol tolerance for branch length checks (if check=TRUE)
#' @param dowser_fields check dowser-specific fields and gapped sequences?
#' @param nproc number of cores to use
#' @details
#' In addition to the existing tree topology, edge length, sequence, and
#' data slot checks, this also verifies \code{tree$parameters} when present
#' -- including \code{build="pml"} trees.
#' @export
dowserObjectEquivalent = function(obj1, obj2, verbose=TRUE, edge_tol=1e-8,
dowser_fields=TRUE, nproc=1){
a_is_timetree <- inherits(obj1$trees[[1]], "treedata")
b_is_timetree <- inherits(obj2$trees[[1]], "treedata")
if(a_is_timetree != b_is_timetree){
stop("Cannot compare a time-tree object against a non-time-tree object")
}
timetree <- FALSE
if(a_is_timetree){
timetree <- TRUE
}
a <- obj1
b <- obj2
a <- a[order(a$clone_id),]
b <- b[order(b$clone_id),]
if(nrow(a) != nrow(b)){
stop("unequal row numbers")
}
if(sum(a$clone_id != b$clone_id)){
stop("different clone ids")
}
if(sum(a$seqs != b$seqs) != 0){
stop("different seq numbers")
}
if(sum(a$locus != b$locus) != 0){
stop("different locus columns")
}
if(dowser_fields && sum(names(a) != names(b)) > 0){
stop("different column names")
}
#for(r in 1:nrow(a)){
checks <- parallel::mclapply(1:nrow(a),function(r)tryCatch({
treecheck <- 0
# check non-tree columns
if(dowser_fields){
for(n in names(a)){
if(n %in% c("data", "trees")){
next
}
vala <- a[[n]][[r]]
valb <- b[[n]][[r]]
if(inherits(vala,"data.frame")){
check <- isTRUE(all.equal(vala, valb))
if(!check){
stop(paste(n, r, "columns not equal"))
}
}
}
}
# check trees
if(timetree){
treea <- a$trees[[r]]@phylo
treeb <- b$trees[[r]]@phylo
treecheckfunc <- treesEquivalent(a$trees[[r]], b$trees[[r]], edge_tol,
clonesa=obj1, clonesb=obj2, check_extended=TRUE, gaps=dowser_fields)
}else{
treea <- a$trees[[r]]
treeb <- b$trees[[r]]
treecheckfunc <- treesEquivalent(treea, treeb, edge_tol,
clonesa=obj1, clonesb=obj2, check_extended=TRUE, gaps=dowser_fields)
}
if(!treecheckfunc){
stop(paste(r, "trees not equivalent (see warnings)"))
}else{
treecheck <- treecheck + 1
}
# check remaining tree info
if(dowser_fields && !setequal(names(treea), names(treeb))){
namediff <- setdiff(names(treea), names(treeb))
if(length(namediff) > 1 || namediff != "node.label"){
print(namediff)
stop(paste(a$clone_id[r], "Tree names not equal"))
}
if("node.label" %in% c(names(treea),names(treeb))){
if(r == 0){
warning("node.label not currently checked, or preserved by writeTreesJSON")
}
}
}else{
treecheck <- treecheck + 1
}
for(n in names(treea)){
if(n %in% c("edge","tip.label","Nnode","edge.length","nodes","node.label")){
next
}
if(n == "parameters" && dowser_fields){
if(is.null(treea[[n]]) && is.null(treeb[[n]])){
treecheck <- treecheck + 1
}else if(is.null(treea[[n]]) || is.null(treeb[[n]])){
stop(paste(a$clone_id[r], n, "not the same"))
}else if(!pmlParamsEqual(treea[[n]], treeb[[n]], edge_tol=edge_tol)){
stop(paste(a$clone_id[r], n, "not the same"))
}else{
treecheck <- treecheck + 1
}
next
}
null <- FALSE
if(sum(!is.na(treea[[n]])) || sum(!is.null(treea[[n]]))){
if(sum(!is.na(treeb[[n]])) || sum(!is.null(treeb[[n]]))){
null <- TRUE
}
}
if(!null && sum(treea[[n]] != treeb[[n]]) != 0){
stop(paste(a$clone_id[r], n, "not the same"))
}else{
treecheck <- treecheck + 1
}
}
# check data
da <- a$data[[r]]
db <- b$data[[r]]
da@data <- da@data[order(da@data$sequence_id),]
db@data <- db@data[order(db@data$sequence_id),]
for(n in slotNames(da)){
if(n == "data"){
dataa = slot(da,n)
datab = slot(db,n)
diffnames = setdiff(names(dataa), names(datab))
if(dowser_fields && sum(!diffnames %in% c("sequence","hlsequence",
"lsequence","tip_order"))){
stop(paste("data names don't match",paste(diffnames, collapse=",")))
}
namechecks <- union(names(dataa), names(datab))
if(!dowser_fields){
namechecks <- intersect(names(dataa), names(datab))
if(!"sequence_id" %in% namechecks || !da@phylo_seq %in% namechecks){
stop("Minimal data columns not present in both")
}
}else{
namechecks <- namechecks[!namechecks %in% c("sequence","hlsequence",
"lsequence","tip_order","sequence_id")]
namechecks <- c(namechecks, "sequence_id", da@phylo_seq)
}
for(name in namechecks){
#if(sum(dataa[[name]] != datab[[name]]) != 0){
if(!isTRUE(all.equal(dataa[[name]], datab[[name]]))){
stop(paste(a$clone_id[r], "data", name, "not the same"))
}
}
}else if(n == "germline" & da@phylo_seq != "sequence"){
next
}else if(n == "lgermline" & da@phylo_seq != "lsequence"){
next
}else if(n == "hlgermline" & da@phylo_seq != "hlsequence"){
next
}else if(!dowser_fields && n %in% c("v_gene","j_gene","junc_len",
"numbers", "region")){
next
}else if(sum(slot(da,n) != slot(db, n)) != 0){
stop(paste(a$clone_id[r], n, "slot is not equal"))
}else{
treecheck <- treecheck + length(slot(da,n))
}
}
#time tree object checks
if(timetree){
dtreea <- a$trees[[r]]
dtreeb <- b$trees[[r]]
# BEAST parameter log
pa <- dtreea@info$parameters
pb <- dtreeb@info$parameters
if(is.null(pa) != is.null(pb)){
stop(paste(a$clone_id[r], "beast parameters presence not the same"))
}
if(!is.null(pa)){
if(!setequal(pa$item, pb$item)){
stop(paste(a$clone_id[r], "beast parameter items not the same"))
}
pb <- pb[match(pa$item, pb$item),]
for(col in setdiff(names(pa), "item")){
va <- pa[[col]]; vb <- pb[[col]]
ok <- isTRUE(all.equal(va, vb)) ||
all(is.na(va) == is.na(vb) & (is.na(va) | va == vb))
if(!ok){
stop(paste(a$clone_id[r], "beast parameter", col, "not the same"))
}
}
}
treecheck <- treecheck + 1
# --- full posterior (@info$trees_posterior/trees_with_traits_posterior/
# parameters_posterior), when present. trees_posterior and
# trees_with_traits_posterior are independent, optional fields, compared
# the same way via comparePosteriorTreeLists(). ---
for(prefix in c("trees_posterior", "trees_with_traits_posterior")){
if(!prefix %in% names(dtreea@info) && !prefix %in% names(dtreeb@info)){
next
}
if(length(dtreea@info[[prefix]]) != length(dtreeb@info[[prefix]])){
stop(paste(a$clone_id[r], prefix, "not the same length"))
}
posterior_comp <- sapply(1:length(dtreea@info[[prefix]]), function(x)
treesEquivalent(
dtreea@info[[prefix]][[x]],
dtreeb@info[[prefix]][[x]],
edge_tol))
if(sum(!posterior_comp) > 0){
stop(paste(a$clone_id[r], prefix, "not equivalent"))
}
treecheck <- treecheck + length(posterior_comp)
}
ppa <- dtreea@info$parameters_posterior
ppb <- dtreeb@info$parameters_posterior
if(is.null(ppa) != is.null(ppb)){
stop(paste(a$clone_id[r], "parameters_posterior presence not the same"))
}
if(!is.null(ppa)){
if(nrow(ppa) != nrow(ppb)){
stop(paste(a$clone_id[r], "parameters_posterior row count not the same"))
}
# Sample can round-trip as "integer" on one side but "double" on the other
key_a <- paste(sprintf("%.0f", ppa$Sample), ppa$parameter)
key_b <- paste(sprintf("%.0f", ppb$Sample), ppb$parameter)
if(!setequal(key_a, key_b)){
stop(paste(a$clone_id[r], "parameters_posterior Sample/parameter keys not the same"))
}
ppb <- ppb[match(key_a, key_b),]
ok <- isTRUE(all.equal(ppa$value, ppb$value)) #||
#all(is.na(ppa$value) == is.na(ppb$value) &
# (is.na(ppa$value) | ppa$value == ppb$value))
if(!ok){
stop(paste(a$clone_id[r], "parameters_posterior values not the same"))
}else{
treecheck <- treecheck + nrow(ppa)
}
}
}
treecheck
#}
},error=function(e)e), mc.cores=nproc)
errors <- sapply(checks, function(x) inherits(x, "error"))
if(sum(errors) == 0){
treecheck <- sum(unlist(checks))
}else{
print(paste(checks[errors]))
stop("Objects not equivalent")
}
if(verbose){
print(paste("Objects equivalent:",treecheck,"tree checks, 0 failures"))
}
return(0)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.