diff --git a/NEWS.md b/NEWS.md index dd419b90..275d5c85 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # NEWS +- v0.5.0 + - Add recursive tree, node and branch types + - Improve testing + - Note, this is a breaking change because the RootedTree type is now an alias for RecursiveTree, not LinkTree for efficiency - v0.4.24 - Fix workflows - v0.4.23 diff --git a/Project.toml b/Project.toml index 15d8c77f..34fef63f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Phylo" uuid = "aea672f4-3940-5932-aa44-993d1c3ff149" author = ["Richard Reeve "] -version = "0.4.24" +version = "0.5.0" [deps] AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" @@ -12,6 +12,8 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" IterableTables = "1c8ee90f-4401-5389-894e-7a04a3dc0f4d" Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" @@ -22,6 +24,7 @@ Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [weakdeps] +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" RCall = "6f49c342-dc21-5d91-9882-a32aef131414" Requires = "ae029012-a4dd-5104-9daa-d747884805df" @@ -37,18 +40,19 @@ Distributions = "0.24, 0.25" Graphs = "1" IterableTables = "1" Missings = "1" +ParserCombinator = "2" Plots = "1" -Printf = "1" +Printf = "1.6" RCall = "0.13" -Random = "1" +Random = "1.6" RecipesBase = "1" Requires = "1" SimpleTraits = "0.9" -Statistics = "1" -Test = "1" +Statistics = "1.6" +Test = "1.6" Tokenize = "0.5" Unitful = "1" -julia = "1" +julia = "1.6" [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/docs/src/man/attributes.md b/docs/src/man/attributes.md index 8c3e8267..3d470363 100644 --- a/docs/src/man/attributes.md +++ b/docs/src/man/attributes.md @@ -49,6 +49,7 @@ getbranch getbranches gettreeinfo validate! +invalidate! branchdims treetype ``` diff --git a/docs/src/man/plotting.md b/docs/src/man/plotting.md index 5c5f09e9..1ad8711e 100644 --- a/docs/src/man/plotting.md +++ b/docs/src/man/plotting.md @@ -97,12 +97,11 @@ the same way ```@example plotting plot(hummers, - size = (400, 800), - linecolor = :orange, linewidth = 5, - markersize = 10, markercolor = :steelblue, markerstrokecolor = :white, - series_annotations = text.(1:nnodes(hummers), 5, :center, :center, :white, - tipfont = (4,)) -) + size = (400, 800), + linecolor = :orange, linewidth = 5, + markersize = 10, markercolor = :steelblue, markerstrokecolor = :white, + series_annotations = text.(1:nnodes(hummers), 5, :center, :center, :white), + tipfont = (4,)) ``` The `marker_group` and `line_group` keywords allow plotting discrete values onto diff --git a/docs/src/man/treetypes.md b/docs/src/man/treetypes.md index a65742f5..59e7daad 100644 --- a/docs/src/man/treetypes.md +++ b/docs/src/man/treetypes.md @@ -19,6 +19,7 @@ This package offers a number of different types of tree, each optimised for a specific usage ```@docs +RecursiveTree LinkTree NamedBinaryTree BinaryTree @@ -27,17 +28,13 @@ PolytomousTree NamedPolytomousTree ``` -## Node types +## Node and Branch types ```@docs +RecursiveElt LinkNode BinaryNode Node -``` - -## Branch types - -```@docs LinkBranch Branch ``` diff --git a/src/API.jl b/src/API.jl index fd80d99f..ca388eb9 100644 --- a/src/API.jl +++ b/src/API.jl @@ -1,6 +1,6 @@ using Phylo using Phylo: Rootedness, Rooted, TreeType, TraversalOrder -using Phylo: AbstractNode, AbstractBranch, AbstractTree +using Phylo: AbstractElt, AbstractNode, AbstractBranch, AbstractTree using SimpleTraits using Unitful @@ -30,7 +30,6 @@ using Unitful """ _prefernodeobjects(::Type{<:AbstractTree}) - _prefernodeobjects(::Type{<:AbstractNode}) Does this tree or node type prefer nodes to be objects or names? Must be implemented for every node type. @@ -41,7 +40,6 @@ _prefernodeobjects(::Type{<:AbstractTree{TT, RT, NL, N, B}}) where """ _preferbranchobjects(::Type{<:AbstractTree}) - _preferbranchobjects(::Type{<:AbstractBranch}) Does this tree or branch type prefer branches to be objects or names? Must be implemented for every branch type. @@ -410,8 +408,7 @@ or a pair) from a tree. Must be implemented for PreferBranchObjects tree types. function _getbranchname end _getbranchname(::AbstractTree{OneTree, RT, NL, N}, pair::Pair{NL, N}) where {RT, NL, N} = pair[1] -_getbranchname(::AbstractTree{OneTree, RT}, - branchname::Int) where RT = branchname +_getbranchname(::AbstractTree{OneTree}, id::Int) = id """ _getbranches(tree::AbstractTree) @@ -515,11 +512,18 @@ function _clearrootheight! end """ _validate!(::AbstractTree) - +Check whether the tree is internally valid. """ function _validate! end _validate!(::AbstractTree) = true +""" + _invalidate!(::AbstractTree, state) + +Confirm that the tree is no longer necessarily valid, and remove cache information. +""" +function _invalidate! end + """ _traversal(tree::AbstractTree, order::TraversalOrder, todo, sofar) @@ -632,7 +636,7 @@ _indegree(tree::AbstractTree{OneTree, Unrooted}, node) = Is there space for a new inbound connection on a node? """ function _hasinboundspace end -_hasinboundspace(tree::AbstractTree{OneTree}, node) = +_hasinboundspace(tree::AbstractTree{OneTree, <: Rooted}, node) = !_hasinbound(tree, node) """ @@ -644,7 +648,7 @@ function _outdegree end _outdegree(tree::AbstractTree{OneTree, <: Rooted}, node) = length(_getoutbounds(tree, node)) _outdegree(tree::AbstractTree{OneTree, Unrooted}, - node::AbstractNode{Unrooted}) = + node::AbstractElt{Unrooted}) = _degree(tree, node) == 0 ? 0 : missing """ @@ -751,9 +755,9 @@ Return the child node(s) for this node. May be implemented for any rooted AbstractNode subtype. """ function _getchildren end -_getchildren(tree::T, node::N) where {RT <: Rooted, NL, N <: AbstractNode, B, T <: AbstractTree{OneTree, RT, NL, N, B}} = +_getchildren(tree::AbstractTree{OneTree, RT}, node::N) where {RT <: Rooted, N <: AbstractElt{RT}} = N[_dst(tree, branch) for branch in _getoutbounds(tree, node)] -_getchildren(tree::T, node::NL) where {RT <: Rooted, NL, N <: AbstractNode, B, T <: AbstractTree{OneTree, RT, NL, N, B}} = +_getchildren(tree::AbstractTree{OneTree, RT, NL}, node::NL) where {RT <: Rooted, NL} = NL[_dst(tree, branch) for branch in _getoutbounds(tree, node)] """ @@ -834,10 +838,10 @@ end # AbstractBranch methods """ - _src(branch::AbstractBranch) + _src(tree, branch) Return source node for a branch. Must be implemented for any rooted -AbstractBranch subtype. +branch type. """ function _src end _src(::T, ::B) where {T, B} = error("No _src() function for $T, $B") diff --git a/src/Interface.jl b/src/Interface.jl index 0f2db891..c8144018 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -7,44 +7,32 @@ using SimpleTraits Returns tree number (OneTree, ManyTrees) from a tree type. """ -treetype(::Type{T}) where {TT <: TreeType, RT, NL, N, B, - T <: AbstractTree{TT, RT, NL, N, B}} = TT +treetype(::Type{T}) where {TT, T <: AbstractTree{TT}} = TT """ roottype(::Type{AbstractTree}) - roottype(::Type{AbstractNode}) - roottype(::Type{AbstractBranch}) + roottype(::Type{AbstractElt}) -Returns root type from a tree type. +Returns root type from a tree, node, branch or other element type. """ -roottype(::Type{T}) where {TT, RT <: Rootedness, NL, N, B, - T <: AbstractTree{TT, RT, NL, N, B}} = RT -roottype(::Type{N}) where {RT <: Rootedness, NL, - N <: AbstractNode{RT, NL}} = RT -roottype(::Type{B}) where {RT <: Rootedness, NL, - B <: AbstractBranch{RT, NL}} = RT +roottype(::Type{T}) where {TT, RT, T <: AbstractTree{TT, RT}} = RT +roottype(::Type{E}) where {RT, E <: AbstractElt{RT}} = RT """ nodenametype(::Type{AbstractTree}) - nodenametype(::Type{AbstractNode}) - nodenametype(::Type{AbstractBranch}) + nodenametype(::Type{AbstractElt}) Returns type of node names from a tree type. """ -nodenametype(::Type{T}) where {TT, RT, NL, N, B, - T <: AbstractTree{TT, RT, NL, N, B}} = NL -nodenametype(::Type{N}) where {RT <: Rootedness, NL, - N <: AbstractNode{RT, NL}} = NL -nodenametype(::Type{B}) where {RT <: Rootedness, NL, - B <: AbstractBranch{RT, NL}} = NL +nodenametype(::Type{T}) where {TT, RT, NL, T <: AbstractTree{TT, RT, NL}} = NL +nodenametype(::Type{E}) where {RT, NL, E <: AbstractElt{RT, NL}} = NL """ nodetype(::Type{AbstractTree}) Returns type of nodes from a tree type. """ -nodetype(::Type{T}) where {TT, RT, NL, N <: AbstractNode, B, - T <: AbstractTree{TT, RT, NL, N, B}} = N +nodetype(::Type{T}) where {TT, RT, NL, NT, T <: AbstractTree{TT, RT, NL, NT}} = NT """ branchnametype(::AbstractTree) @@ -52,16 +40,15 @@ nodetype(::Type{T}) where {TT, RT, NL, N <: AbstractNode, B, Returns type of branch names from a branch type. """ branchnametype(::Type{<: AbstractTree}) = Int -branchnametype(::Type{<: AbstractNode}) = Int -branchnametype(::Type{<: AbstractBranch}) = Int +branchnametype(::Type{<: AbstractElt}) = Int """ branchtype(::Type{AbstractTree}) Returns type of branches from a tree type. """ -branchtype(::Type{T}) where {TT, RT, NL, N, B <: AbstractBranch, - T <: AbstractTree{TT, RT, NL, N, B}} = B +branchtype(::Type{T}) where {TT, RT, NL, NT, BT <: AbstractElt, + T <: AbstractTree{TT, RT, NL, NT, BT}} = BT """ treenametype(::Type{AbstractTree}) @@ -1241,6 +1228,13 @@ function validate!(tree::T) where return _validate!(tree) end +""" + invalidate!(tree::AbstractTree, state = missing) + +Confirm that the tree is no longer necessarily valid, and remove cache information. +""" +invalidate!(tree::AbstractTree, state = missing) = _invalidate!(tree, state) + """ traversal(::AbstractTree, ::TraversalOrder) traversal(::AbstractTree, ::TraversalOrder, init) diff --git a/src/Iterators.jl b/src/Iterators.jl index af29c926..c023bfd9 100644 --- a/src/Iterators.jl +++ b/src/Iterators.jl @@ -28,14 +28,14 @@ end abstract type AbstractNodeIterator{T <: AbstractTree} <: AbstractTreeIterator{T} end function length(ni::It) where It <: AbstractNodeIterator - return ni.filterfn === nothing ? _nnodes(ni.tree) : + return isnothing(ni.filterfn) ? _nnodes(ni.tree) : count(val -> ni.filterfn(ni.tree, _getnode(ni.tree, val)), ni) end abstract type AbstractBranchIterator{T <: AbstractTree} <: AbstractTreeIterator{T} end function length(bi::It) where It <: AbstractBranchIterator - return bi.filterfn === nothing ? _nbranches(bi.tree) : + return isnothing(bi.filterfn) ? _nbranches(bi.tree) : count(val -> bi.filterfn(bi.tree, _getbranch(bi.tree, val)), bi) end @@ -155,7 +155,7 @@ eltype(bi::BranchNameIterator{T}) where T <: AbstractTree = branchnametype(T) import Base: iterate function iterate(tree::AbstractTree, state = nothing) - if state === nothing + if isnothing(state) return first(gettrees(tree)), 1 elseif ntrees(tree) > state return collect(gettrees(tree))[state], state + 1 @@ -166,15 +166,15 @@ end function iterate(ni::NodeIterator, state = nothing) nodes = _getnodes(ni.tree) - if state === nothing + if isnothing(state) result = iterate(nodes) else result = iterate(nodes, state) end - result === nothing && return nothing + isnothing(result) && return nothing - if ni.filterfn === nothing + if isnothing(ni.filterfn) return _getnode(ni.tree, result[1]), result[2] end @@ -182,7 +182,7 @@ function iterate(ni::NodeIterator, state = nothing) node = _getnode(ni.tree, val) while !ni.filterfn(ni.tree, node) result = iterate(nodes, state) - result === nothing && return nothing + isnothing(result) && return nothing val, state = result node = _getnode(ni.tree, val) end @@ -192,15 +192,15 @@ end function iterate(ni::NodeNameIterator, state = nothing) nodes = _getnodes(ni.tree) - if state === nothing + if isnothing(state) result = iterate(nodes) else result = iterate(nodes, state) end - result === nothing && return nothing + isnothing(result) && return nothing - if ni.filterfn === nothing + if isnothing(ni.filterfn) return _getnodename(ni.tree, result[1]), result[2] end @@ -208,7 +208,7 @@ function iterate(ni::NodeNameIterator, state = nothing) node = _getnode(ni.tree, val) while !ni.filterfn(ni.tree, node) result = iterate(nodes, state) - result === nothing && return nothing + isnothing(result) && return nothing val, state = result node = _getnode(ni.tree, val) end @@ -219,15 +219,15 @@ end function iterate(bi::BranchIterator, state = nothing) branches = _getbranches(bi.tree) - if state === nothing + if isnothing(state) result = iterate(branches) else result = iterate(branches, state) end - result === nothing && return nothing + isnothing(result) && return nothing - if bi.filterfn === nothing + if isnothing(bi.filterfn) return _getbranch(bi.tree, result[1]), result[2] end @@ -235,7 +235,7 @@ function iterate(bi::BranchIterator, state = nothing) branch = _getbranch(bi.tree, val) while !bi.filterfn(bi.tree, branch) result = iterate(branches, state) - result === nothing && return nothing + isnothing(result) && return nothing val, state = result branch = _getbranch(bi.tree, val) end @@ -245,15 +245,15 @@ end function iterate(bi::BranchNameIterator, state = nothing) branches = _getbranches(bi.tree) - if state === nothing + if isnothing(state) result = iterate(branches) else result = iterate(branches, state) end - result === nothing && return nothing + isnothing(result) && return nothing - if bi.filterfn === nothing + if isnothing(bi.filterfn) return _getbranchname(bi.tree, result[1]), result[2] end @@ -261,7 +261,7 @@ function iterate(bi::BranchNameIterator, state = nothing) branch = _getbranch(bi.tree, val) while !bi.filterfn(bi.tree, branch) result = iterate(branches, state) - result === nothing && return nothing + isnothing(result) && return nothing val, state = result branch = _getbranch(bi.tree, val) end diff --git a/src/LinkTree.jl b/src/LinkTree.jl index 676089e3..babb0875 100644 --- a/src/LinkTree.jl +++ b/src/LinkTree.jl @@ -86,9 +86,6 @@ const LB{RT, LenUnits} = LinkBranch{RT, String, Dict{String, Any}, LenUnits} const LN{RT, LenUnits} = LinkNode{RT, String, Dict{String, Any}, LB{RT, LenUnits}} const LT{RT, TD, LenUnits} = LinkTree{RT, String, LN{RT, LenUnits}, LB{RT, LenUnits}, TD} const LTD{RT, LenUnits} = LT{RT, Dict{String, Any}, LenUnits} -const RootedTree = LTD{OneRoot, Float64} -const ManyRootTree = LTD{ManyRoots, Float64} -const UnrootedTree = LTD{Unrooted, Float64} # LinkBranch methods function LinkBranch(name::Int, @@ -221,9 +218,6 @@ function _removeconnection!(tree::AbstractTree, end # LinkTree methods -const TREENAME = "Tree" -const NODENAME = "Node" - import Phylo.API: _validate! function _validate!(tree::LinkTree{RT, NL, N, B, TD}) where {RT, NL, N, B, TD} tree.isvalid = true diff --git a/src/Phylo.jl b/src/Phylo.jl index 7432c9df..ba72e3f2 100644 --- a/src/Phylo.jl +++ b/src/Phylo.jl @@ -18,6 +18,9 @@ interact cleanly with other phylogenetic packages. """ module Phylo +const TREENAME = "Tree" +const NODENAME = "Node" + import Base: Pair, Tuple, show, eltype, length, getindex import Graphs: src, dst, indegree, outdegree, degree abstract type Rootedness end @@ -32,13 +35,19 @@ struct OneTree <: TreeType end struct ManyTrees <: TreeType end export OneTree, ManyTrees -abstract type AbstractNode{RootType <: Rootedness, NodeLabel} end -abstract type AbstractBranch{RootType <: Rootedness, NodeLabel} end +abstract type BranchingType end +struct BinaryBranching <: BranchingType end +struct PolytomousBranching <: BranchingType end +export BinaryBranching, PolytomousBranching + +abstract type AbstractElt{RootType <: Rootedness, NodeLabel} end +abstract type AbstractNode{RootType, NodeLabel} <: AbstractElt{RootType, NodeLabel} end +abstract type AbstractBranch{RootType, NodeLabel} <: AbstractElt{RootType, NodeLabel} end using Distances abstract type AbstractTree{TT <: TreeType, RT <: Rootedness, NL, - N <: AbstractNode{RT, NL}, - B <: AbstractBranch{RT, NL}} <: Distances.UnionMetric + Node <: AbstractElt{RT, NL}, + Branch <: AbstractElt{RT, NL}} <: Distances.UnionMetric end export AbstractTree @@ -68,7 +77,7 @@ export _getbranchdata, _setbranchdata!, _branchdatatype export _hasheight, _getheight, _setheight! export _hasparent, _getparent, _getancestors export _haschildren, _getchildren, _getdescendants -export _validate!, _traversal, _branchdims +export _validate!, _invalidate!, _traversal, _branchdims export _getleafnames, _getleaves, _resetleaves!, _nleaves, _nnodes, _nbranches export HoldsNodeData, MatchTreeNameType @@ -102,7 +111,7 @@ export getnodenames, getnodename, hasnode, getnode, getnodes, nnodes export getleafnames, getleaves, nleaves, getinternalnodes, ninternal export getbranchnames, getbranchname, hasbranch, getbranch, getbranches, nbranches export hasrootheight, getrootheight, setrootheight! -export validate!, traversal, branchdims +export validate!, invalidate!, traversal, branchdims @deprecate addnode! createnode! @deprecate addnodes! createnodes! @@ -149,7 +158,10 @@ export PolytomousTree, NamedPolytomousTree include("LinkTree.jl") export LinkBranch, LinkNode, LinkTree -export RootedTree, ManyRootTree, UnrootedTree + +include("RecursiveTree.jl") +export RecursiveElt, RecursiveBranch, RecursiveNode, RecursiveTree +export RootedTree, ManyRootTree, UnrootedTree, BinaryRootedTree include("routes.jl") export branchhistory, branchfuture, branchroute diff --git a/src/RecursiveTree.jl b/src/RecursiveTree.jl new file mode 100644 index 00000000..5a97c411 --- /dev/null +++ b/src/RecursiveTree.jl @@ -0,0 +1,711 @@ +using Unitful + +""" + struct RecursiveElt <: AbstractElt + +A type for branches or nodes in a RecursiveTree, allowing navigation of the tree without +using the tree object itself. +""" +Base.@kwdef mutable struct RecursiveElt{RT, NL, MyType <: AbstractElt{RT, NL}, MyData, + TheirType <: AbstractElt{RT, NL}, TheirData, + BT <: BranchingType, LenUnits <: Number} <: + AbstractElt{RT, NL} + + name::Union{NL, Nothing} = nothing + id::Union{Int, Missing} = missing + in::Union{RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, + BT, LenUnits}, Nothing} = nothing + conns::Vector{RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, + BT, LenUnits}} = + RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, BT, LenUnits}[] + data::MyData = _emptydata(MyData) + length::Union{LenUnits, Missing} = missing +end + +const RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits} = + RecursiveElt{RT, NL, AbstractNode{RT, NL}, NodeData, + AbstractBranch{RT, NL}, BranchData, BT, LenUnits} + +const RecursiveBranch{RT, NL, NodeData, BranchData, BT, LenUnits} = + RecursiveElt{RT, NL, AbstractBranch{RT, NL}, BranchData, + AbstractNode{RT, NL}, NodeData, BT, LenUnits} + +RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}(name::NL, data::NodeData = nothing) where + {RT, NL, NodeData, BranchData, BT, LenUnits} = + RecursiveNode{RT, NL, NodeData, BranchData, + BT, LenUnits}(name, nothing, + RecursiveBranch{RT, NL, NodeData, + BranchData, LenUnits}[], + data, missing) + +""" + struct RecoursiveTree <: AbstractTree + +A phylogenetic tree type containing RecursiveElts as both nodes and branches, +allowing navigation of the tree using only the node and branch elements. +""" +mutable struct RecursiveTree{RT, NL, NodeData, BranchData, BT <: BranchingType, LenUnits, TD} <: + AbstractTree{OneTree, RT, NL, + RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}, + RecursiveBranch{RT, NL, NodeData, BranchData, BT, LenUnits}} + + name::String + nodedict::Dict{NL, Int} + roots::Vector{RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}} + nodes::Vector{Union{RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}, + Missing}} + branches::Vector{Union{RecursiveBranch{RT, NL, NodeData, BranchData, BT, LenUnits}, + Missing}} + data::Dict{String, Any} + tipdata::TD + rootheight::Union{LenUnits, Missing} + isvalid::Union{Bool, Missing} + cache::Dict{TraversalOrder, + Vector{RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}}} + + function RecursiveTree{RT, NL, NodeData, BranchData, + BT, LenUnits, TD}(tipnames::Vector{NL} = NL[]; + name::String = TREENAME, + tipdata::TD = _emptydata(TD), + rootheight::Union{LenUnits, Missing} = missing, + validate::Bool = false) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} + NT = RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits} + BrT = RecursiveBranch{RT, NL, NodeData, BranchData, BT, LenUnits} + tree = new{RT, NL, NodeData, BranchData, + BT, LenUnits, TD}(name, Dict{NL, NT}(), NT[], + Union{NT, Missing}[], Union{BrT, Missing}[], + Dict{String, Any}(), + tipdata, rootheight, missing, + Dict{TraversalOrder, Vector{NT}}()) + + if !isempty(tipnames) + createnodes!(tree, tipnames) + elseif !isnothing(tree.tipdata) && !isempty(tree.tipdata) + createnodes!(tree, unique(keys(tree.tipdata))) + end + + if validate + validate!(tree) + else + tree.isvalid = missing + end + + return tree + end +end + +function RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}(leafinfos::TD) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} + + leafnames = unique(info[1] for info in getiterator(leafinfos)) + return RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}(leafnames; tipdata = leafinfos) +end + +import Phylo.API: _prefernodeobjects, _preferbranchobjects +_prefernodeobjects(::Type{<:RecursiveTree}) = true +_prefernodeobjects(::Type{<:RecursiveNode}) = true +_prefernodeobjects(::Type{<:RecursiveElt}) = true +_preferbranchobjects(::Type{<:RecursiveTree}) = true +_preferbranchobjects(::Type{<:RecursiveBranch}) = true +_preferbranchobjects(::Type{<:RecursiveElt}) = true + +import Phylo.API: _validate! +function _validate!(tree::RecursiveTree{RT, NL, NodeData, BranchData, + BT, LenUnits, TD}) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} + + tree.isvalid = true + + if !_matchlabels(NL, _tiplabeltype(TD)) + tree.isvalid = false + error("Tree $(_gettreename(tree)) has inconsistent node and tip label types $NL and $(_tiplabeltype(TD))") + end + + if !isnothing(tree.tipdata) && !isempty(tree.tipdata) + tree.isvalid &= (Set(keys(tree.tipdata)) == Set(getleafnames(tree))) + end + + nr = nroots(tree) + if RT ≡ OneRoot + if nr ≠ 1 + @warn "Wrong number of roots for $RT tree ($nr)" + tree.isvalid = false + end + elseif RT ≡ ManyRoots + if nr < 1 + @warn "Wrong number of roots for $RT tree ($nr)" + tree.isvalid = false + end + end + + if length(tree.nodedict) ≠ _nnodes(tree) + @warn "Number of nodes in node lookup is inconsistent with " * + "node vector for tree ($(length(tree.nodedict)) ≠ $(_nnodes(tree))" + tree.isvalid = false + else + for (i, node) in enumerate(tree.nodes) + if !ismissing(node) + if i ≠ node.id || i ≠ tree.nodedict[node.name] + @warn "Mismatch between node $(node.name) id $(node.id) and " * + "references in tree: $i and $(get(tree.nodedict, node.name, missing))" + tree.isvalid = false + else + for branch in node.conns + if node ≢ branch.in && node ∉ branch.conns + tree.isvalid = false + @warn "Mismatch between node name $(node.name) and its connections" + end + end + if RT <: Rooted + if _hasinbound(tree, node) + branch = node.in + if node ≢ branch.in && node ∉ branch.conns + tree.isvalid = false + @warn "Mismatch between node name $(node.name) and its connections" + end + end + if BT ≡ BinaryBranching && length(getoutbounds(tree, node)) > 2 + tree.isvalid = false + @warn "Node name $(node.name) has too many outbound connections" + end + else # Unrooted + if BT ≡ BinaryBranching && length(getoutbounds(tree, node)) > 3 + tree.isvalid = false + @warn "Node name $(node.name) has too many outbound connections" + end + end + end + end + end + end + + for key in keys(tree.cache) + if length(tree.cache[key]) ≠ _nnodes(tree) + @warn "Number of nodes in tree cache with key $key is inconsistent " * + "with tree ($(length(tree.cache[key])) ≠ $(_nnodes(tree)))" + tree.isvalid = false + end + end + + for (i, branch) in enumerate(tree.branches) + if !ismissing(branch) + if i ≠ branch.id + @warn "Mismatch between branch id $(branch.id) and reference in tree $i" + tree.isvalid = false + else + for node in branch.conns + if branch ≢ node.in && branch ∉ node.conns + tree.isvalid = false + @warn "Mismatch between branch id $(branch.id) and its connections" + end + end + if RT <: Rooted + node = branch.in + if branch ≢ node.in && branch ∉ node.conns + tree.isvalid = false + @warn "Mismatch between branch id $(branch.id) and its connections" + end + end + end + end + end + + return tree.isvalid +end + +import Phylo.API: _invalidate! +function _invalidate!(tree::RecursiveTree, state = missing) + empty!(tree.cache) + tree.isvalid = state +end + +# Type aliases + +const ReB{RT, BT, LenUnits} = RecursiveBranch{RT, String, Dict{String, Any}, Dict{String, Any}, BT, LenUnits} +const ReN{RT, BT, LenUnits} = RecursiveNode{RT, String, Dict{String, Any}, Dict{String, Any}, BT, LenUnits} +const ReT{RT, TD, BT, LenUnits} = RecursiveTree{RT, String, Dict{String, Any}, Dict{String, Any}, BT, LenUnits, TD} +const ReTD{RT, BT, LenUnits} = ReT{RT, Dict{String, Any}, BT, LenUnits} +const BinaryRootedTree = ReTD{OneRoot, BinaryBranching, Float64} +const RootedTree = ReTD{OneRoot, PolytomousBranching, Float64} +const ManyRootTree = ReTD{ManyRoots, PolytomousBranching, Float64} +const UnrootedTree = ReTD{Unrooted, PolytomousBranching, Float64} + +# Types matches + +_emptydata(::Type{Data}) where Data = Data() +_emptydata(::Type{RecursiveNode{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = _newdata(NodeData) +_emptydata(::Type{RecursiveBranch{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = _newdata(BranchData) + +_tiplabeltype(::Type{Nothing}) = Nothing +_tiplabeltype(::Type{<: Dict{NL}}) where NL = NL +_tiplabeltype(::Type{RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} = _tiplabeltype(TD) + +_matchlabels(::Type{S}, ::Type{T}) where {S, T} = false +_matchlabels(::Type{S}, ::Type{S}) where S = true +_matchlabels(::Type{S}, ::Type{Nothing}) where S = true + +# Retrieving trees + +import Phylo.API: _treenametype +_treenametype(::Type{<: RecursiveTree}) = String + +import Phylo.API: _gettreename +_gettreename(tree::RecursiveTree) = tree.name + +# Information about leaves in a single store on the tree + +import Phylo.API: _leafinfotype +_leafinfotype(::Type{<:RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} = TD + +import Phylo.API: _getleafinfo +_getleafinfo(tree::RecursiveTree) = tree.tipdata + +import Phylo.API: _setleafinfo! +_setleafinfo!(tree::RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}, leafinfo::TD) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} = (tree.tipdata = leafinfo) + +# Retrieving nodes + +import Phylo.API: _getroots +_getroots(tree::RecursiveTree{<: Rooted}) = tree.roots +_getroots(::RecursiveTree{Unrooted}) = error("Unrooted trees do not have roots") + +import Phylo.API: _nnodes +_nnodes(tree::RecursiveTree) = count((!)∘ismissing, tree.nodes) + +import Phylo.API: _getnodes +_getnodes(tree::RecursiveTree) = skipmissing(tree.nodes) + +import Phylo.API: _getnodenames +_getnodenames(tree::RecursiveTree) = keys(tree.nodedict) + +import Phylo.API: _hasnode +_hasnode(tree::RecursiveTree{RT, NL}, name::NL) where {RT, NL} = + haskey(tree.nodedict, name) +_hasnode(tree::RecursiveTree{RT, NL}, node::N) where + {RT, NL, N <: RecursiveNode{RT, NL}} = + haskey(tree.nodedict, node.name) + +import Phylo.API: _getnode +_getnode(::RecursiveTree, node::RecursiveNode) = node +_getnode(tree::RecursiveTree{RT, NL}, name::NL) where {RT, NL} = tree.nodes[tree.nodedict[name]] + +import Phylo.API: _getnodename +_getnodename(::RecursiveTree, node::RecursiveNode) = node.name +_getnodename(::RecursiveTree{RT, NL}, name::NL) where {RT, NL} = name + +# Creating and destroy nodes + +import Phylo.API: _createnode! +function _createnode!(tree::RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}, + name::Union{NL, Missing}, data::NodeData = _emptydata(NodeData)) where + {RT <: Rooted, NL, NodeData, BranchData, BT, LenUnits, TD} + + NT = RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits} + nodename = ismissing(name) ? _newnodelabel(tree) : name + _hasnode(tree, nodename) && error("Node $nodename already exists in tree.") + id = length(tree.nodes) + 1 + node = NT(name = nodename, id = id, data = data) + push!(tree.nodes, node) + tree.nodedict[nodename] = id + push!(tree.roots, node) + + _invalidate!(tree) + return node +end + +function _createnode!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, BT, LenUnits, TD}, + name::Union{NL, Missing}, data::NodeData = _emptydata(NodeData)) where + {NL, NodeData, BranchData, BT, LenUnits, TD} + + NT = RecursiveNode{Unrooted, NL, NodeData, BranchData, BT, LenUnits} + nodename = ismissing(name) ? _newnodelabel(tree) : name + _hasnode(tree, nodename) && error("Node $nodename already exists in tree.") + id = length(tree.nodes) + 1 + node = NT(name = nodename, id = id, data = data) + push!(tree.nodes, node) + tree.nodedict[nodename] = id + + _invalidate!(tree) + return node +end + +import Phylo.API: _deletenode! +function _deletenode!(tree::RecursiveTree, node::RecursiveNode) + (haskey(tree.nodedict, node.name) && + tree.nodedict[node.name] == node.id && + tree.nodes[node.id] ≡ node) || + error("Node $(node.name) is not in tree $(tree.name), cannot be deleted") + + # Does nothing for unrooted tree, as inbound is never set + !isnothing(node.in) && _deletebranch!(tree, node.in) + + # Delete outbound connections of rooted tree, all connections of unrooted + while _degree(tree, node) > 0 + _deletebranch!(tree, first(node.conns)) + end + + tree.nodes[tree.nodedict[node.name]] = missing + delete!(tree.nodedict, node.name) + filter!(n -> n ≢ node, tree.roots) + _invalidate!(tree) + return true +end + +# Retrieving and editing connections on nodes + +import Phylo.API: _hasinbound +_hasinbound(::RecursiveTree, node::RecursiveNode{<: Rooted}) = !isnothing(node.in) + +import Phylo.API: _degree +_degree(::RecursiveTree, node::RecursiveNode{Unrooted}) = length(node.conns) + +import Phylo.API: _getinbound +_getinbound(::RecursiveTree, node::RecursiveNode{<: Rooted}) = node.in + +import Phylo.API: _getoutbounds +_getoutbounds(::RecursiveTree, node::RecursiveNode{<: Rooted}) = node.conns + +import Phylo.API: _getconnections +_getconnections(::RecursiveTree, node::RecursiveNode{Unrooted}) = node.conns + +import Phylo.API: _addinbound! +function _addinbound!(tree::RecursiveTree{RT}, + node::RecursiveNode{RT}, + branch::RecursiveBranch{RT}) where RT <: Rooted + _hasinbound(tree, node) && + error("RecursiveNode $(node.name) already has an inbound connection") + node.in = branch + filter!(n -> n ≠ node, tree.roots) + _invalidate!(tree) +end + +import Phylo.API: _removeinbound! +function _removeinbound!(tree::RecursiveTree{RT}, + node::RecursiveNode{RT}, + branch::RecursiveBranch{RT} = node.in) where RT <: Rooted + _hasinbound(tree, node) || error("Node $(node.name) has no inbound connection") + node.in ≡ branch || + error("Node $(node.name) has no inbound connection from branch $(branch.id)") + node.in = nothing + push!(tree.roots, node) + _invalidate!(tree) +end + +import Phylo.API: _hasoutboundspace +_hasoutboundspace(::RecursiveTree{<: Rooted, NL, TheirData, MyData, + BinaryBranching, LenUnits, TD}, + node::RecursiveNode{<: Rooted, NL, TheirData, MyData, + BinaryBranching, LenUnits}) where + {NL, TheirData, MyData, TD, LenUnits} = length(node.conns) < 2 + +import Phylo.API: _hasspace +_hasspace(::RecursiveTree{Unrooted, NL, TheirData, MyData, + BinaryBranching, LenUnits, TD}, +node::RecursiveNode{Unrooted, NL, TheirData, MyData, + BinaryBranching, LenUnits}) where + {NL, TheirData, MyData, LenUnits, TD} = length(node.conns) < 3 + +import Phylo.API: _addoutbound! +function _addoutbound!(tree::RecursiveTree{RT}, + node::RecursiveNode{RT}, + branch::RecursiveBranch{RT}) where RT <: Rooted + _hasoutboundspace(tree, node) || + error("Node $(from.name) has no outbound space") + + push!(node.conns, branch) + _invalidate!(tree) +end + +import Phylo.API: _removeoutbound! +function _removeoutbound!(tree::RecursiveTree{RT}, + node::RecursiveNode{RT}, + branch::RecursiveBranch{RT}) where RT <: Rooted + if branch ∉ _getoutbounds(tree, node) + error("Node $(node.name) does not have outbound connection to branch $(branch.id)") + end + filter!(p -> p ≢ branch, node.conns) + _invalidate!(tree) +end + +import Phylo.API: _addconnection! +function _addconnection!(tree::RecursiveTree{Unrooted}, + node::RecursiveNode{Unrooted}, + branch::RecursiveBranch{Unrooted}) + if !_hasspace(tree, node) + error("Node $(node.name) does not have space for a new connection") + end + push!(node.conns, branch) + _invalidate!(tree) +end + +import Phylo.API: _removeconnection! +function _removeconnection!(tree::RecursiveTree{Unrooted}, + node::RecursiveNode{Unrooted}, + branch::RecursiveBranch{Unrooted}) + if branch ∉ _getconnections(tree, node) + error("Node $(node.name) does not have connection to branch $(branch.id)") + end + filter!(p -> p ≢ branch, node.conns) + _invalidate!(tree) +end + +# Retrieving branches + +import Phylo.API: _nbranches +_nbranches(tree::RecursiveTree) = count((!)∘ismissing, tree.branches) + +import Phylo.API: _getbranches +_getbranches(tree::RecursiveTree) = skipmissing(tree.branches) + +import Phylo.API: _getbranchnames +_getbranchnames(tree::RecursiveTree) = [b.id for b in skipmissing(tree.branches)] + +import Phylo.API: _hasbranch +_hasbranch(tree::RecursiveTree, id::Int) = + 1 ≤ id ≤ length(tree.branches) && !ismissing(tree.branches[id]) +_hasbranch(tree::RecursiveTree, branch::RecursiveBranch) = + branch ≡ tree.branches[branch.id] + +import Phylo.API: _getbranch +_getbranch(tree::RecursiveTree, id::Int) = tree.branches[id] + +import Phylo.API: _getbranchname +_getbranchname(::RecursiveTree, branch::RecursiveBranch) = branch.id + +# Branch length info + +import Phylo.API: _branchdims +_branchdims(T::Type{<:RecursiveTree}) = _branchdims(branchtype(T)) +_branchdims(::Type{<:RecursiveBranch{RT, NL, NodeData, BranchData, BT, LenUnits}}) where + {RT, NL, NodeData, BranchData, BT, LenUnits} = dimension(LenUnits) + +import Phylo.API: _getlength +_getlength(::RecursiveTree, branch::RecursiveBranch) = branch.length + +# Retrieving connections on branches + +import Phylo.API: _src +_src(::RecursiveTree, branch::RecursiveBranch{<:Rooted}) = branch.in + +import Phylo.API: _dst +_dst(::RecursiveTree, branch::RecursiveBranch{<:Rooted}) = branch.conns[1] + +import Phylo.API: _conn +_conn(::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}, + exclude::RecursiveNode{Unrooted}) = + exclude ≡ branch.conns[1] ? branch.conns[2] : + (exclude ≡ branch.conns[2] ? branch.conns[1] : + error("Branch $(branch.id) not connected to node $(exclude.name)")) + +import Phylo.API: _conns +_conns(::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}) = branch.conns + +# Information about individual nodes stored on the nodes + +import Phylo.API: _nodedatatype +_nodedatatype(::Type{<:RecursiveTree{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = NodeData +_nodedatatype(::Type{<:RecursiveNode{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = NodeData + +import Phylo.API: _getnodedata +_getnodedata(::RecursiveTree, node::RecursiveNode) = node.data + +import Phylo.API: _setnodedata! +_setnodedata!(::RecursiveTree{RT, NL, NodeData, BranchData}, + node::RecursiveNode{RT, NL, NodeData, BranchData}, + data::NodeData) where {RT, NL, NodeData, BranchData} = (node.data = data) + +# Information about individual branches stored on the branches + +import Phylo.API: _branchdatatype +_branchdatatype(::Type{<:RecursiveTree{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = BranchData +_branchdatatype(::Type{<:RecursiveBranch{RT, NL, NodeData, BranchData}}) where + {RT, NL, NodeData, BranchData} = BranchData + +import Phylo.API: _getbranchdata +_getbranchdata(::RecursiveTree, branch::RecursiveBranch) = branch.data + +import Phylo.API: _setbranchdata! +_setbranchdata!(::RecursiveTree{RT, NL, NodeData, BranchData}, + branch::RecursiveBranch{RT, NL, NodeData, BranchData}, + data::BranchData) where {RT, NL, NodeData, BranchData} = + (branch.data = data) + +# New label methods + +import Phylo.API: _newnodelabel +function _newnodelabel(tree::RecursiveTree{RT, String}) where RT + id = length(tree.nodes) + 1 + name = NODENAME * " $id" + return haskey(tree.nodedict, name) ? + _newlabel(collect(getnodenames(tree)), NODENAME) : name +end + +import Phylo.API: _newbranchlabel +_newbranchlabel(tree::RecursiveTree) = length(tree.branches) + 1 + +import Phylo.API: _createbranch! +function _createbranch!(tree::RecursiveTree{RT, NL, NodeData, BranchData, BT, LenUnits, TD}, + from::RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}, + to::RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}, + len::Union{LenUnits, Missing} = missing, + name = missing, + data::BranchData = _emptydata(BranchData)) where + {RT <: Rooted, NL, NodeData, BranchData, BT, LenUnits, TD} + + if ismissing(name) + name = length(tree.branches) + 1 + end + + ismissing(len) || len ≥ zero(len) || + error("Branch length must be positive or missing (no recorded length), not $len") + + _hasinboundspace(tree, to) || + error("Node $(to.name) already has an inbound connection") + + _hasoutboundspace(tree, from) || + error("Node $(from.name) has no outbound space") + + branch = RecursiveBranch{RT, NL, NodeData, BranchData, + BT, LenUnits}(id = name, in = from, conns = [to], + length = len, data = data) + + if 1 ≤ name ≤ length(tree.branches) + tree.branches[name] = branch + else + l = length(tree.branches) + 1 + if name > l + resize!(tree.branches, name) + tree.branches[l:name-1] .= missing + tree.branches[name] = branch + else + push!(tree.branches, branch) + end + end + + _addoutbound!(tree, from, branch) + _addinbound!(tree, to, branch) + + _invalidate!(tree) + return branch +end + +function _createbranch!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, BT, LenUnits, TD}, + from::RecursiveNode{Unrooted, NL, NodeData, BranchData, BT, LenUnits}, + to::RecursiveNode{Unrooted, NL, NodeData, BranchData, BT, LenUnits}, + len::Union{LenUnits, Missing} = missing, + name = missing, + data::BranchData = _emptydata(BranchData)) where + {Unrooted, NL, NodeData, BranchData, BT, LenUnits, TD} + + if ismissing(name) + name = length(tree.branches) + 1 + end + + ismissing(len) || len ≥ zero(len) || + error("Branch length must be positive or missing (no recorded length), not $length") + + branch = RecursiveBranch{Unrooted, NL, NodeData, BranchData, + BT, LenUnits}(id = name, conns = [to, from], + length = len, data = data) + + _hasspace(tree, from) || + error("Node $(from.name) has no space for new connections") + + _hasspace(tree, to) || + error("Node $(to.name) has no space for new connections") + + if 1 ≤ name ≤ length(tree.branches) + tree.branches[name] = branch + else + l = length(tree.branches) + 1 + if name > l + resize!(tree.branches, name) + tree.branches[l:name-1] .= missing + tree.branches[name] = branch + else + push!(tree.branches, branch) + end + end + + _addconnection!(tree, from, branch) + _addconnection!(tree, to, branch) + + _invalidate!(tree) + return branch +end + +function _deletebranch!(tree::RecursiveTree{RT}, branch::RecursiveBranch{RT}) where RT <: Rooted + _removeoutbound!(tree, branch.in, branch) + _removeinbound!(tree, branch.conns[1], branch) + tree.branches[branch.id] = missing + _invalidate!(tree) + return true +end + +function _deletebranch!(tree::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}) + _removeconnection!(tree, branch.conns[1], branch) + _removeconnection!(tree, branch.conns[2], branch) + tree.branches[branch.id] = missing + _invalidate!(tree) + return true +end + +import Base.show +function show(io::IO, node::RecursiveNode{Unrooted}) + nc = length(node.conns) + print(io, "Unrooted RecursiveNode '$(node.name)' ") + if nc == 0 + print(io, "with no connections.") + elseif nc == 1 + print(io, "with 1 connection (branch $(node.conns[1].id))") + else + print(io, "with $nc outbound connections (branches $(getfield.(node.conns, :id)))") + end +end + +function show(io::IO, node::RecursiveNode{RT}) where RT <: Rooted + print(io, "$RT RecursiveNode $(node.name), ") + no = length(node.conns) + if isnothing(node.in) + if no == 0 + print(io, "an isolated node with no connections.") + elseif no == 1 + print(io, "a root node with 1 outbound connection" * + " (branch $(node.conns[1].id))") + else + print(io, "a root node with $nc outbound connections" * + " (branches $(getfield.(node.conns, :id)))") + end + else + if no == 0 + print(io, "a leaf with an incoming connection (branch $(node.in.id)).") + elseif no == 1 + print(io, "an internal node with 1 inbound and 1 outbound connection " * + "(branches $(node.in.id) and $(node.conns[1].id))") + else + print(io, "an internal node with 1 inbound and $nc outbound connections" * + " (branches $(node.in.id) and $(getfield.(node.conns, :id)))") + end + end +end + +function show(io::IO, branch::RecursiveBranch{Unrooted}) + print(io, "Unrooted RecursiveBranch $(branch.id), connecting nodes" * + " $(branch.conns[1].name) and $(branch.conns[2].name)" * + (ismissing(branch.length) ? "" : " (length $(branch.length)).")) +end + +function show(io::IO, branch::RecursiveBranch{RT}) where RT <: Rooted + print(io, "$RT RecursiveBranch $(branch.id), from node $(branch.in.name)" * + " to node $(branch.conns[1].name)" * + (ismissing(branch.length) ? "" : " (length $(branch.length)).")) +end diff --git a/src/newick.jl b/src/newick.jl index e4d0665a..401ee793 100644 --- a/src/newick.jl +++ b/src/newick.jl @@ -1,3 +1,68 @@ +#= +# Tree → Subtree ";" +# Subtree → Leaf | Internal +# Leaf → Name +# Internal → "(" BranchSet ")" Name_or_Support +# BranchSet → Branch | Branch "," BranchSet +# Branch → Subtree Length +# Name_or_Support → empty | string | number +# Length → empty | ":" FPNum +# FPNum number + +# open_meta + ("[^"]*"+|[^,=\s]+) + spc + (=\s*(\{[^=}]*\}|"[^"]*"+|[^,]+))? + close_meta + +using ParserCombinator + +spc = Drop(Star(Space())) +blank = E"" +quotes = E"\"" +open_meta = E"[&" # |> "__start_meta" +close_meta = E"]" # |> "__end_meta" +open_branchset = E"(" # |> "__start_subtree" +close_branchset = E")" # |> "__end_subtree" +next_one = E"," # |> "__next_one" +equals = E"=" + +@with_names begin +@with_pre spc begin + str = p"[a-zA-Z_][a-zA-Z\d_]*" | (quotes + p"[\"]+" + quotes) + name = str | blank + length_sep = E":" # |> "__length" + length_val = Parse(p"-?(\d*\.?\d+|\d+\.\d*)([eE]-?\d+)?", Float64) + datum = (p"\"[^\"]*\"" | p"[^,=\s\"]+") + spc + equals + spc + (p"{[^=}]*}" | p"\"[^\"]*\"" | p"[^,]+") + data = Delayed() + data.matcher = datum + ((next_one + data) | blank) + metacomments = (open_meta + data + close_meta) | blank + node_metacomments = metacomments + branch_metacomments = metacomments + blength = (length_sep + spc + branch_metacomments + length_val) | blank + leaf = (str | blank) + node_metacomments # |> "__leaf" + subtree = Delayed() + branch = subtree + node_metacomments + blength + branchset = Delayed() + branchset.matcher = branch + ((next_one + branchset) | blank) + fpnum = Parse(p"(\d*\.?\d+|\d+\.\d*)", Float64) + internal = open_branchset + branchset + close_branchset + (fpnum | name) + node_metacomments # |> "__internal" + subtree.matcher = leaf | internal + endnewick = E";" + spc + Eos() # |> "__end_tree" + newick = subtree + endnewick +end +end + +@time open("test/Qian2016.tree", "r") do io + parse_try(io, newick) +end +io = open("test/Qian2016.tree", "r") +out = read(io, String) +parse_dbg(out, Trace(newick)) +@time parse_one(out, newick) + +tree = "(A9CIT6:0.59280764,P51981:0.55926221,(Q8A861:0.99105703,((Q81IL5:0.76431643,((((A1B198:0.94287572,(Q2U1E8:0.71410953,Q5LT20:0.55480466)0.975579:0.21734008)1.000000:0.55075633,(Q92YR6:1.11236546,(((Q13PB7:1.33807955,(Q161M1:1.17720944,A1AYL4:0.93440931)0.784619:0.18922325)0.878426:0.09769089,((((Q1ASJ4:1.28537785,(A8LS88:0.87558406,(C1DMY1:0.14671933,(P77215:0.02112667,Q8ZNF9:0.01593493)1.000000:0.35900384)1.000000:0.81055398)0.999947:0.27041496)0.403932:0.04809748,(A4YVM8:1.35286455,(Q9RKF7:0.83804265,((Q8ZL58:0.21550115,Q12GE3:0.23170031)1.000000:0.65091551,(Q7CU39:0.71681321,(Q8P3K2:0.27030998,(Q1GLV3:0.34268834,Q7L5Y1:0.42965239)0.843769:0.09133540)1.000000:1.04593860)0.978319:0.19792131)0.995516:0.18393058)0.684889:0.10148106)0.965570:0.12638685)0.999013:0.10597436,(((A0A0H3LM82:1.53892471,(O06741:1.56982104,(G0L7B8:0.68911617,A9CEQ8:0.63642012)0.999148:0.26000097)0.760390:0.07007390)0.534387:0.04760860,(A0A0H3LT39:0.95322505,(Q3HKK5:1.65509086,(A8H7M5:1.21743086,A8H9D1:0.47372214)0.711619:0.14571049)0.992889:0.20930969)0.824619:0.10359095)0.957749:0.09170993,(((Q8ZNH1:0.35062372,Q5NN22:0.45517287)1.000000:0.51175191,(Q7D1T6:0.17783006,Q63IJ7:0.15483880)1.000000:0.87953156)0.879253:0.11535090,(Q28RT0:1.01784576,(B9JNP7:1.11261669,(B2UCA8:0.76348582,(((A6M2W4:0.21565444,A4W7D6:0.17558479)1.000000:0.37879482,(D4GJ14:0.06604958,(C6CBG9:0.01850349,B5R541:0.03323447)0.999985:0.05738427)1.000000:0.29155266)1.000000:0.31191076,((C6D9S0:0.03964480,Q8FHC7:0.03209453)1.000000:0.13486764,((A4XF23:0.22295702,(Q1QT89:0.18122799,B3PDB1:0.14414146)0.999998:0.06015729)0.968272:0.04285536,(B0T0B1:0.05224760,Q9AAR4:0.07234240)1.000000:0.14812779)0.999678:0.08813897)1.000000:0.36147407)1.000000:0.47112287)0.928316:0.13341811)0.550875:0.06620266)0.961209:0.13640648)1.000000:0.27744895)0.988757:0.09058974)0.866782:0.06631759,(A9CL63:0.35266112,Q92ZS5:0.19783599)1.000000:1.18369094)0.402267:0.02752454)0.999888:0.19319607,(Q9F3A5:0.58548261,((Q3KB33:0.33553686,(A0R5B5:0.21484600,D6Y7Y6:0.27848012)1.000000:0.24287080)0.942008:0.15046146,(Q1QUN0:0.25266568,(C0WBB5:0.25833170,(P0AES2:0.09059530,A6VQF1:0.05673552)1.000000:0.13075826)0.999738:0.14696003)1.000000:0.81155149)1.000000:0.47139015)1.000000:0.50975221)0.966695:0.13031427)0.863808:0.08043994)0.973288:0.08656638,(Q5P025:0.97873157,(C5CFI0:1.08126948,((A0A0H3KH80:0.00000001,(A0A0H2WWB5:0.00000001,Q53635:0.00000001)-1.000000:0.00000001)-1.000000:1.77379709,((Q838J7:0.42816989,Q927X3:0.43775742)1.000000:0.24306201,(Q5SJX8:0.32151423,Q9RYA6:0.31040549)1.000000:0.38120655)0.761411:0.11121216)0.449066:0.07920285)1.000000:0.65145569)0.934431:0.11633312)0.809578:0.05916034,(A0QTN8:1.01771865,((Q8DJP8:2.14313928,(Q8NN12:0.75309341,(P05404:0.60462842,(Q4K9X1:0.32854393,A6T9N5:0.34558716)1.000000:0.22924230)0.999793:0.18933082)0.610131:0.11821100)0.811426:0.14174769,(A8HTB8:0.54448819,(Q5LM96:0.23356964,Q28SI7:0.15669417)1.000000:0.47534595)1.000000:0.49079583)0.999955:0.20013288)0.717358:0.06863833)0.729022:0.08518511)0.998543:0.11833475,(Q607C7:1.08575204,(((O34508:0.48939847,B0TZW0:1.11933860)0.879395:0.12702564,(Q9WXM1:0.80769788,(A9B055:0.36657533,(A5UXJ3:0.34547016,A9GEI3:0.26128229)0.974762:0.12479711)1.000000:0.52751375)1.000000:0.33586771)0.610795:0.07157613,(Q11T61:0.92177095,Q834W6:0.44934225)0.999732:0.14429535)0.996719:0.09875344)0.787853:0.04813874)0.983918:0.24536033)1.000000:0.66916649);" +tree = "(A9CIT6[&a=2]:0.59280764,P51981:0.55926221,(Q8A861:0.99105703,((Q81IL5:0.76431643,((((A1B198:0.94287572,(Q2U1E8:0.71410953,Q5LT20:0.55480466)0.975579:0.21734008)1.000000:0.55075633,(Q92YR6:1.11236546,(((Q13PB7:1.33807955,(Q161M1:1.17720944,A1AYL4:0.93440931)0.784619:0.18922325)0.878426:0.09769089,((((Q1ASJ4:1.28537785,(A8LS88:0.87558406,(C1DMY1:0.14671933,(P77215:0.02112667,Q8ZNF9:0.01593493)1.000000:0.35900384)1.000000:0.81055398)0.999947:0.27041496)0.403932:0.04809748)))))))))))));" +parse_dbg(tree, Trace(newick)) + +=# + using Tokenize using Tokenize.Lexers using Missings @@ -23,12 +88,12 @@ isTRANSLATE(token) = isIDENTIFIER(token, "translate") isEND(token) = (token.kind == T.END) | isIDENTIFIER(token, "end") | isIDENTIFIER(token, "endblock") function iterateskip(tokens, state = nothing) - result = (state === nothing) ? iterate(tokens) : iterate(tokens, state) - result === nothing && return nothing + result = isnothing(state) ? iterate(tokens) : iterate(tokens, state) + isnothing(result) && return nothing token, state = result while isWHITESPACE(token) result = iterate(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end ut = untokenize(token) @@ -52,7 +117,7 @@ function tokensgetkey(token, state, tokens, finished::Function = isEQ) push!(sofar, untokenize(token)) end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end return token, state, join(sofar) @@ -63,7 +128,7 @@ function checktosemi(test::Function, token, state, tokens) return false end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.SEMICOLON tokenerror(token, ";") @@ -77,23 +142,23 @@ function parsevector(token, state, tokens, ::Type{TY}, sgn) where TY <: Real if token.kind == T.PLUS sgn = +; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result elseif token.kind == T.MINUS sgn = -; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end push!(vec, sgn(parse(TY, untokenize(token)))) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.COMMA && token.kind != T.RBRACE tokenerror(token, ",") elseif token.kind == T.COMMA result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -103,18 +168,18 @@ end function parsevector(token, state, tokens) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result sgn = +; if token.kind == T.MINUS sgn = -; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result elseif token.kind == T.PLUS result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -132,13 +197,13 @@ function parsevector(token, state, tokens) push!(vec, untokenize(token)) end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.COMMA && token.kind != T.RBRACE tokenerror(token, ",") elseif token.kind == T.COMMA result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -149,13 +214,13 @@ end function parsedict(token, state, tokens) dict = Dict{String, Any}() result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.AND tokenerror(token, "&") else result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -163,7 +228,7 @@ function parsedict(token, state, tokens) token, state, key = tokensgetkey(token, state, tokens, isEQorRSQUARE) if token.kind != T.RSQUARE # Allow [&R] as a valid (empty) dict result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind == T.LBRACE token, state, value = parsevector(token, state, tokens) @@ -172,12 +237,12 @@ function parsedict(token, state, tokens) if token.kind == T.PLUS sgn = +; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result elseif token.kind == T.MINUS sgn = -; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -194,13 +259,13 @@ function parsedict(token, state, tokens) dict[key] = value result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.COMMA && token.kind != T.RSQUARE tokenerror(token, ", or ]") elseif token.kind == T.COMMA result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -208,7 +273,7 @@ function parsedict(token, state, tokens) if token.kind == T.RSQUARE result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -253,7 +318,7 @@ function parsenode(token, state, tokens, tree::TREE, if token.kind == T.COLON foundcolon = true result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -267,19 +332,19 @@ function parsenode(token, state, tokens, tree::TREE, if token.kind == T.COLON || foundcolon if token.kind == T.COLON result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end sgn = +; if token.kind == T.PLUS sgn = +; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result elseif token.kind == T.MINUS sgn = -; result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end if token.kind ∈ [T.INTEGER, T.FLOAT] @@ -289,11 +354,11 @@ function parsenode(token, state, tokens, tree::TREE, tokenerror(token, "a length") end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end - return token, state, myname + return token, state, getnodename(tree, myname) end function parsenewick!(token, state, tokens, tree::TREE, @@ -303,7 +368,7 @@ function parsenewick!(token, state, tokens, tree::TREE, mychildren = Dict{NL, Dict{String, Any}}() while (token.kind != T.RPAREN) & (token.kind != T.ENDMARKER) result = iterateskip(tokens, state) - result === nothing && + isnothing(result) && error("Tree ended at depth $depth before right bracket") token, state = result if token.kind == T.LPAREN @@ -315,7 +380,7 @@ function parsenewick!(token, state, tokens, tree::TREE, end end result = iterateskip(tokens, state) - result === nothing && + isnothing(result) && error("Tree ended at depth $depth before" * (depth > 0 ? "right bracket" : "semicolon")) token, state = result @@ -333,7 +398,7 @@ function parsenewick!(token, state, tokens, tree::TREE, if token.kind == T.SEMICOLON # Am at end of tree result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result else error("At end of tree, but not ';'") @@ -349,7 +414,7 @@ end function parsenewick(tokens::Tokenize.Lexers.Lexer, ::Type{TREE}) where {RT, N, B, TREE <: AbstractTree{OneTree, RT, String, N, B}} result = iterateskip(tokens) - if result === nothing + if isnothing(result) error("Unexpected end of file at start of newick file") end token, state = result @@ -410,30 +475,30 @@ function parsetaxa(token, state, tokens, taxa) tokenerror(token, "Dimensions") end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result token, state, ntaxstr = tokensgetkey(token, state, tokens) if lowercase(ntaxstr) != "ntax" error("Unexpected label '$ntaxstr=' not 'ntax='") end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result ntax = parse(Int64, untokenize(token)) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind != T.SEMICOLON tokenerror(token, ";") end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if !isTAXLABELS(token) tokenerror(token, "Taxlabels") end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result while token.kind != T.SEMICOLON && token.kind != T.ENDMARKER name = untokenize(token) @@ -441,17 +506,17 @@ function parsetaxa(token, state, tokens, taxa) name = name[2:end-1] end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result taxa[name] = name if token.kind == T.LSQUARE while token.kind != T.RSQUARE result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -460,7 +525,7 @@ function parsetaxa(token, state, tokens, taxa) end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if !checktosemi(isEND, token, state, tokens) tokenerror(token, "End;") @@ -473,16 +538,16 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where notaxa = isempty(taxa) if isTRANSLATE(token) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result while token.kind != T.SEMICOLON && token.kind != T.ENDMARKER short = untokenize(token) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result proper = untokenize(token) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if haskey(taxa, proper) delete!(taxa, proper) @@ -495,12 +560,12 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where end if token.kind == T.COMMA result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end @@ -509,7 +574,7 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where numTrees = 0 while isTREE(token) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result token, state, treename = tokensgetkey(token, state, tokens, t -> t.kind ∈ [T.LSQUARE, T.EQ]) @@ -528,7 +593,7 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where tokenerror(token, "=") else result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind == T.LSQUARE token, state, _ = parsedict(token, state, tokens) @@ -538,7 +603,7 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where tokenerror(token, "(") else result = parsenewick!(token, state, tokens, trees[treename], taxa) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -547,7 +612,7 @@ function parsetrees(token, state, tokens, ::Type{TREE}, taxa) where end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result return token, state, trees, treedata end @@ -561,38 +626,38 @@ function parsenexus(token, state, tokens, ::Type{TREE}) where if token.kind == T.LSQUARE while token.kind != T.RSQUARE result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result else result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if checktosemi(isTAXA, token, state, tokens) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result token, state = parsetaxa(token, state, tokens, taxa) elseif checktosemi(isTREES, token, state, tokens) result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result token, state, trees, treedata = parsetrees(token, state, tokens, TREE, taxa) else @warn "Unexpected nexus block '$(untokenize(token))', skipping..." result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result while !checktosemi(isEND, token, state, tokens) && token.kind != T.ENDMARKER result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result end end @@ -608,11 +673,11 @@ function parsenexus(tokens::Tokenize.Lexers.Lexer, ::Type{TREE}) where {RT, NL, N, B, TREE <: AbstractTree{OneTree, RT, NL, N, B}} result = iterateskip(tokens) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result if token.kind == T.COMMENT && lowercase(untokenize(token)) == "#nexus" result = iterateskip(tokens, state) - result === nothing && return nothing + isnothing(result) && return nothing token, state = result return parsenexus(token, state, tokens, TREE) else diff --git a/src/plot.jl b/src/plot.jl index 43b62e87..3b85cd3d 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -196,7 +196,7 @@ descendants. This creates a clearer tree for plotting. The process is also called "ladderizing" the tree. Use `rev=true` to reverse the sorting order. """ -function Base.sort!(tree::AbstractTree; rev = false) +function Base.sort!(tree::T; rev = false) where T <: AbstractTree function loc!(clade::String) if isleaf(tree, clade) return 1 @@ -204,7 +204,11 @@ function Base.sort!(tree::AbstractTree; rev = false) sizes = map(loc!, getchildren(tree, clade)) node = getnode(tree, clade) - node.other .= node.other[sortperm(sizes, rev = rev)] + if T <: LinkTree + node.other .= node.other[sortperm(sizes, rev = rev)] + elseif T <: RecursiveTree + node.conns .= node.conns[sortperm(sizes, rev = rev)] + end sum(sizes) + 1 end @@ -282,7 +286,7 @@ function _circle_transform_segments(xs, ys) push!(rety, _ycirc(_y[3], _x[3]), NaN) end i = 1 - while !(i === nothing) && i < length(xs) + while !isnothing(i) && i < length(xs) j = findnext(isnan, xs, i) - 1 _transform_seg(view(xs,i:j), view(ys, i:j)) i = j + 2 @@ -307,7 +311,7 @@ julia> evolve(tree) = map_depthfirst((val, node) -> val + randn(), 0., tree, Flo """ function map_depthfirst(FUN, start, tree, eltype = nothing) root = first(nodenamefilter(isroot, tree)) - eltype === nothing && (eltype = typeof(FUN(start, root))) + isnothing(eltype) && (eltype = typeof(FUN(start, root))) ret = Vector{eltype}() function local!(val, node) push!(ret, val) diff --git a/src/routes.jl b/src/routes.jl index ce394ef1..4245eb36 100644 --- a/src/routes.jl +++ b/src/routes.jl @@ -12,8 +12,8 @@ using AxisArrays nextnode = pop!(nodestoprocess) push!(nodesprocessed, nextnode) if hasinbound(tree, nextnode) - push!(branches, _getinbound(tree, nextnode)) - push!(nodestoprocess, _getparent(tree, nextnode)) + push!(branches, getinbound(tree, nextnode)) + push!(nodestoprocess, getparent(tree, nextnode)) end end return branches, nodesprocessed @@ -29,8 +29,8 @@ end nextnode = pop!(nodestoprocess) push!(nodesprocessed, nextnode) if hasinbound(tree, nextnode) - push!(branches, _getinbound(tree, nextnode)) - push!(nodestoprocess, _getparent(tree, nextnode)) + push!(branches, getinbound(tree, nextnode)) + push!(nodestoprocess, getparent(tree, nextnode)) end end return branches, nodesprocessed @@ -45,8 +45,8 @@ end while !isempty(nodestoprocess) nextnode = pop!(nodestoprocess) push!(nodesprocessed, nextnode) - append!(branches, _getoutbounds(tree, nextnode)) - append!(nodestoprocess, _getchildren(tree, nextnode)) + append!(branches, getoutbounds(tree, nextnode)) + append!(nodestoprocess, getchildren(tree, nextnode)) end return branches, nodesprocessed end @@ -60,8 +60,8 @@ end while !isempty(nodestoprocess) nextnode = pop!(nodestoprocess) push!(nodesprocessed, nextnode) - append!(branches, _getoutbounds(tree, nextnode)) - append!(nodestoprocess, _getchildren(tree, nextnode)) + append!(branches, getoutbounds(tree, nextnode)) + append!(nodestoprocess, getchildren(tree, nextnode)) end return branches, nodesprocessed end diff --git a/test/run_rcall.jl b/test/run_rcall.jl index 077bc114..330876e3 100644 --- a/test/run_rcall.jl +++ b/test/run_rcall.jl @@ -11,8 +11,9 @@ global skipR = !rcopy(R"require(ape)") @testset "For $TreeType" for TreeType in (skipR ? [] : [NamedTree, NamedBinaryTree, BinaryTree{ManyRoots, DataFrame, Vector{Float64}}, + Phylo.LTD{OneRoot, Float64}, Phylo.LTD{ManyRoots, Float64}, RootedTree, ManyRootTree]) - + @testset "Testing with R rtree($i)" for i in 10:10:50 rt = rcall(:rtree, i) jt = rcopy(TreeType, rt) diff --git a/test/test_Interface.jl b/test/test_Interface.jl index 582ef59d..9d3458d7 100644 --- a/test/test_Interface.jl +++ b/test/test_Interface.jl @@ -19,6 +19,7 @@ end @testset "For $TreeType" for TreeType in [NamedTree, NamedBinaryTree, BinaryTree{ManyRoots, DataFrame, Vector{Float64}}, + Phylo.LTD{OneRoot, Float64}, Phylo.LTD{ManyRoots, Float64}, RootedTree, ManyRootTree] @test treetype(TreeType) == OneTree diff --git a/test/test_LinkTree.jl b/test/test_LinkTree.jl index ad42c672..7076651d 100644 --- a/test/test_LinkTree.jl +++ b/test/test_LinkTree.jl @@ -14,13 +14,6 @@ jdb = DataFrame(species = observations, count = 1:4) @testset "RootedTree()" begin name = "internal" - lts = RootedTree(species) - @test nodedatatype(typeof(lts)) ≡ Dict{String, Any} - @test branchdatatype(typeof(lts)) ≡ Dict{String, Any} - @test leafinfotype(typeof(lts)) ≡ Dict{String, Any} - @test_nowarn createnode!(lts, name) - @test createbranch!(lts, name, species[1]) ∈ getbranches(lts) - ltdf = Phylo.LT{OneRoot, DataFrame, Float64}(df) @test nodedatatype(typeof(ltdf)) ≡ Dict{String, Any} @test branchdatatype(typeof(ltdf)) ≡ Dict{String, Any} @@ -31,17 +24,9 @@ end @testset "UnrootedTree()" begin name = "internal" - urts = UnrootedTree(species) - @test nodedatatype(typeof(urts)) ≡ Dict{String, Any} - @test branchdatatype(typeof(urts)) ≡ Dict{String, Any} - @test leafinfotype(typeof(urts)) ≡ Dict{String, Any} - @test_nowarn createnode!(urts, name) - @test createbranch!(urts, name, species[1]) ∈ getbranches(urts) - - RT = Unrooted - LB = LinkBranch{RT, String, Nothing, Float64} - LN = LinkNode{RT, String, Vector{Int}, LB} - ltjdb = LinkTree{RT, String, LN, LB, typeof(jdb)}(jdb) + LB = LinkBranch{Unrooted, String, Nothing, Float64} + LN = LinkNode{Unrooted, String, Vector{Int}, LB} + ltjdb = LinkTree{Unrooted, String, LN, LB, typeof(jdb)}(jdb) @test nodedatatype(typeof(ltjdb)) ≡ Vector{Int} @test branchdatatype(typeof(ltjdb)) ≡ Nothing @test leafinfotype(typeof(ltjdb)) ≡ typeof(jdb) diff --git a/test/test_RecursiveTree.jl b/test/test_RecursiveTree.jl new file mode 100644 index 00000000..4a8bd9a0 --- /dev/null +++ b/test/test_RecursiveTree.jl @@ -0,0 +1,83 @@ +module TestRecursiveTree + +using Phylo +using DataFrames + +using Test +using IterableTables: getiterator + +species = ["Dog", "Cat", "Human", "Potato"] +ntips = 10 +df = DataFrame(species = species, count = [10, 20, 3, 31]) +observations = ["Dog", "Cat", "Dog", "Dog"] +jdb = DataFrame(species = observations, count = 1:4) + +@testset "RootedTree()" begin + name = "internal" + rts = RootedTree(species) + @test nodedatatype(typeof(rts)) ≡ Dict{String, Any} + @test branchdatatype(typeof(rts)) ≡ Dict{String, Any} + @test leafinfotype(typeof(rts)) ≡ Dict{String, Any} + @test_nowarn createnode!(rts, name) + @test createbranch!(rts, name, species[1]) ∈ getbranches(rts) + + rtdf = Phylo.ReT{OneRoot, DataFrame, BinaryBranching, Float64}(df) + @test nodedatatype(typeof(rtdf)) ≡ Dict{String, Any} + @test branchdatatype(typeof(rtdf)) ≡ Dict{String, Any} + @test leafinfotype(typeof(rtdf)) ≡ DataFrame + @test_nowarn createnode!(rtdf, name) + @test createbranch!(rtdf, name, species[1]) ∈ getbranches(rtdf) + @test createbranch!(rtdf, name, species[2]) ∈ getbranches(rtdf) + @test_throws ErrorException createbranch!(rtdf, name, species[3]) + + rtdfp = Phylo.ReT{OneRoot, DataFrame, PolytomousBranching, Float64}(df) + @test nodedatatype(typeof(rtdfp)) ≡ Dict{String, Any} + @test branchdatatype(typeof(rtdfp)) ≡ Dict{String, Any} + @test leafinfotype(typeof(rtdfp)) ≡ DataFrame + @test_nowarn createnode!(rtdfp, name) + @test createbranch!(rtdfp, name, species[1]) ∈ getbranches(rtdfp) + @test createbranch!(rtdfp, name, species[2]) ∈ getbranches(rtdfp) + b = createbranch!(rtdfp, name, species[3]) + @test b ∈ getbranches(rtdfp) + @test deletebranch!(rtdfp, b) + @test createbranch!(rtdfp, name, species[3]) ∈ getbranches(rtdfp) + @test_throws ErrorException createbranch!(rtdfp, name, species[2]) +end + +@testset "UnrootedTree()" begin + name = "internal" + urts = Phylo.ReTD{Unrooted, BinaryBranching, Float64}(species) + @test nodedatatype(typeof(urts)) ≡ Dict{String, Any} + @test branchdatatype(typeof(urts)) ≡ Dict{String, Any} + @test leafinfotype(typeof(urts)) ≡ Dict{String, Any} + @test_nowarn createnode!(urts, name) + @test createbranch!(urts, name, species[1]) ∈ getbranches(urts) + @test createbranch!(urts, name, species[2]) ∈ getbranches(urts) + @test createbranch!(urts, name, species[3]) ∈ getbranches(urts) + @test_throws ErrorException createbranch!(urts, name, species[4]) + + urtsp = UnrootedTree(species) + @test nodedatatype(typeof(urtsp)) ≡ Dict{String, Any} + @test branchdatatype(typeof(urtsp)) ≡ Dict{String, Any} + @test leafinfotype(typeof(urtsp)) ≡ Dict{String, Any} + @test_nowarn createnode!(urtsp, name) + @test createbranch!(urtsp, name, species[1]) ∈ getbranches(urtsp) + @test createbranch!(urtsp, name, species[2]) ∈ getbranches(urtsp) + @test createbranch!(urtsp, name, species[3]) ∈ getbranches(urtsp) + @test createbranch!(urtsp, name, species[4]) ∈ getbranches(urtsp) + + LB = RecursiveBranch{Unrooted, String, Vector{Int}, Nothing, BinaryBranching, Float64} + LN = RecursiveNode{Unrooted, String, Vector{Int}, Nothing, BinaryBranching, Float64} + rtjdb = RecursiveTree{Unrooted, String, Vector{Int}, Nothing, BinaryBranching, Float64, typeof(jdb)}(jdb) + @test nodedatatype(typeof(rtjdb)) ≡ Vector{Int} + @test branchdatatype(typeof(rtjdb)) ≡ Nothing + @test leafinfotype(typeof(rtjdb)) ≡ typeof(jdb) + @test_nowarn createnode!(rtjdb, name) + b = createbranch!(rtjdb, name, observations[1], data = nothing) + @test b ∈ getbranches(rtjdb) + @test deletebranch!(rtjdb, b) + @test createbranch!(rtjdb, name, observations[1], data = nothing) ∈ getbranches(rtjdb) + +end + +end diff --git a/test/test_newick.jl b/test/test_newick.jl index cc7ddcc1..304ed01a 100644 --- a/test/test_newick.jl +++ b/test/test_newick.jl @@ -37,7 +37,7 @@ using Test end @testset "For $TreeType" for TreeType in - [NamedTree, RootedTree, ManyRootTree] + [NamedTree, Phylo.LTD{OneRoot, Float64}, Phylo.LTD{ManyRoots, Float64}, RootedTree, ManyRootTree] @test nnodes(parsenewick("((,),(,,));", TreeType)) == 8 @test ["where", "when it's good", "Not mine", "MyLeaf", "next"] ⊆ nodenameiter(parsenewick("""((MyLeaf,"when it's good",next), diff --git a/test/test_plot.jl b/test/test_plot.jl index d09fc243..60f61de2 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -3,10 +3,27 @@ using Test using Phylo using Plots +using Random @testset "Plots" begin - tree = open(parsenewick, Phylo.path("hummingbirds.tree")) - @test all(getproperty.([plot(tree), plot(tree, treetype = :fan)], :n) .== 1) + tree = open(parsenewick, Phylo.path("hummingbirds.tree")); + @test length(plot(tree).subplots) == 1 + trait = map_depthfirst((val, node) -> val + randn(), 0., tree, Float64) + @test plot(sort!(tree), treetype = :fan, line_z = trait, linecolor = :RdYlBu, linewidth = 5, showtips = false).n == 1 + @test plot(tree, markersize = 10, markercolor = :steelblue, + markerstrokecolor = :white, + series_annotations = text.(1:nnodes(tree), 5, :center, :center, :white), + tipfont = (4,)).init + + @enum TemperatureTrait lowTempPref midTempPref highTempPref + tempsampler = SymmetricDiscreteTrait(tree, TemperatureTrait, 0.4, "Temperature") + rand!(tempsampler, tree) + + ## and plot it + @test plot(tree, showtips = true, + marker_group = "Temperature", + legend = :topleft, msc = :white, treetype = :fan, + c = [:red :blue :green]).init end end diff --git a/test/test_rand.jl b/test/test_rand.jl index ab6f32b3..9326c446 100644 --- a/test/test_rand.jl +++ b/test/test_rand.jl @@ -9,7 +9,7 @@ using Test @testset "Nonultrametric()" begin # Create a 10 tip tree nu = Nonultrametric(10) - @test eltype(nu) == RootedTree + @test eltype(nu) == Phylo.LTD{OneRoot, Float64} @test validate!(rand(nu)) @test Set(getleafnames(rand(nu))) == Set(getleafnames(rand(nu))) # Create a tree with named tips diff --git a/test/test_routes.jl b/test/test_routes.jl index 4b20ce07..ac0873df 100644 --- a/test/test_routes.jl +++ b/test/test_routes.jl @@ -6,7 +6,8 @@ using Test @testset "Routes" begin @testset "For $TreeType" for TreeType in [NamedTree, NamedBinaryTree, - RootedTree, ManyRootTree] + Phylo.LTD{OneRoot, Float64}, Phylo.LTD{ManyRoots, Float64}, + RootedTree, ManyRootTree] species = ["Dog", "Cat", "Human", "Potato"] tree = TreeType(species) nr = createnode!(tree) @@ -48,6 +49,7 @@ using Test length(branchfuture(tree, root)) nn = first(nodenamefilter(isleaf, tree)) @test length(branchhistory(tree, nn)) == length(getancestors(tree, nn)) + @test length(branchfuture(tree, root)) == length(nodefuture(tree, root)) - 1 @test Set(getancestors(tree, nn)) ⊆ Set(nodehistory(tree, nn)) n = first(nodefilter(isleaf, tree)) @test length(branchhistory(tree, n)) == length(getancestors(tree, n)) diff --git a/test/test_show.jl b/test/test_show.jl index b906c81d..318aee3b 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -13,7 +13,7 @@ do not give warnings or errors, not that they are correct! ntips = 10 a = IOBuffer() @testset "Nonultrametric{$TreeType}" for TreeType in - [NamedTree, NamedPolytomousTree, RootedTree] + [NamedTree, NamedPolytomousTree, Phylo.LTD{OneRoot, Float64}, RootedTree] nt = rand(Nonultrametric{TreeType}(ntips)) @test_nowarn show(a, nt)