From 8d443f7e38807d3ae4327d4ca138f521d1849285 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 00:58:33 +0000 Subject: [PATCH 01/22] Add ParserCombinator and restore Julia 1.0 compat --- .github/workflows/testing.yaml | 1 + Project.toml | 12 +++++++----- src/plot.jl | 10 +++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0c8771ff..0d7f2e7f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -15,6 +15,7 @@ jobs: strategy: matrix: julia-version: + - '1.0' - '1.6' - '1' os: diff --git a/Project.toml b/Project.toml index 15d8c77f..4279ccf5 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.4.25" [deps] AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" @@ -12,6 +12,7 @@ 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" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" @@ -37,15 +38,16 @@ Distributions = "0.24, 0.25" Graphs = "1" IterableTables = "1" Missings = "1" +ParserCombinator = "2" Plots = "1" -Printf = "1" +Printf = "<0.0.1, 1" RCall = "0.13" -Random = "1" +Random = "<0.0.1, 1" RecipesBase = "1" Requires = "1" SimpleTraits = "0.9" -Statistics = "1" -Test = "1" +Statistics = "<0.0.1, 1" +Test = "<0.0.1, 1" Tokenize = "0.5" Unitful = "1" julia = "1" diff --git a/src/plot.jl b/src/plot.jl index 43b62e87..542a9984 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -15,8 +15,8 @@ using RecipesBase lz = get(plotattributes, :line_z, nothing) mz = get(plotattributes, :marker_z, nothing) - isnothing(lz) || (line_z := _handlez(lz, tree, n)) - isnothing(mz) || (marker_z := _handlez(mz, tree, n)) + lz === nothing || (line_z := _handlez(lz, tree, n)) + mz === nothing || (marker_z := _handlez(mz, tree, n)) mg = _handlez(marker_group, tree, n) lg = _handlez(line_group, tree, n) @@ -68,7 +68,7 @@ struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; marker_ dend.x, dend.y end if !isempty(dend.marker_x) || sa !== nothing - if isnothing(dend.marker_group) + if dend.marker_group === nothing @series begin seriestype := :scatter sa !== nothing && (series_annotations := sa) @@ -125,7 +125,7 @@ end x, y end if !isempty(fan.marker_x) || sa !== nothing - if isnothing(fan.marker_group) + if fan.marker_group === nothing @series begin seriestype := :scatter sa !== nothing && (series_annotations := sa) @@ -167,7 +167,7 @@ end _mylength(x) = 1 _mylength(x::AbstractVector) = length(x) function _handlemarkers(plotattributes, marker_group, tree, d, h, names) - isnothing(marker_group) || (plotattributes[:marker_group] = marker_group) + marker_group === nothing || (plotattributes[:marker_group] = marker_group) markerfields = filter(x->occursin(r"marker", String(x)), keys(plotattributes)) isempty(markerfields) && return (Float64[], Float64[]) maxlength = maximum([_mylength(plotattributes[k]) for k in markerfields]) From b511eaed3afff8e4adffac6c2ef35b57dd02eb9b Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 02:02:16 +0000 Subject: [PATCH 02/22] Add in weak Plots dependency --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 4279ccf5..0c1f4452 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ 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" @@ -23,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" From 0861b4452eb090df6a6fa0f71c79045843663fa1 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 02:23:08 +0000 Subject: [PATCH 03/22] Extend plot testing --- test/test_plot.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_plot.jl b/test/test_plot.jl index d09fc243..1e032f15 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -6,7 +6,9 @@ using Plots @testset "Plots" begin tree = open(parsenewick, Phylo.path("hummingbirds.tree")) - @test all(getproperty.([plot(tree), plot(tree, treetype = :fan)], :n) .== 1) + @test plot(tree).n == 1 + trait = map_depthfirst((val, node) -> val + randn(), 0., tree, Float64) + @test plot(tree, treetype = :fan, line_z = trait, linecolor = :RdYlBu, linewidth = 5, showtips = false).n == 1 end end From 18c9b0fec6f0921fc4d8c2343fb2a888c80accd1 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 02:55:04 +0000 Subject: [PATCH 04/22] More plot testing --- test/test_plot.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_plot.jl b/test/test_plot.jl index 1e032f15..926c4f95 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -6,9 +6,13 @@ using Plots @testset "Plots" begin tree = open(parsenewick, Phylo.path("hummingbirds.tree")) - @test plot(tree).n == 1 + @test length(plot(tree).subplots) == 1 trait = map_depthfirst((val, node) -> val + randn(), 0., tree, Float64) @test plot(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 end end From 007a298487b05ee4522d0b66fca8b7f92a3eff8b Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 03:56:38 +0000 Subject: [PATCH 05/22] More testing --- .github/workflows/testing.yaml | 2 +- test/test_plot.jl | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0d7f2e7f..ee5154ef 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -16,7 +16,7 @@ jobs: matrix: julia-version: - '1.0' - - '1.6' + - 'lts' - '1' os: - ubuntu-latest diff --git a/test/test_plot.jl b/test/test_plot.jl index 926c4f95..c5b083c6 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -3,16 +3,27 @@ using Test using Phylo using Plots +using Random @testset "Plots" begin - tree = open(parsenewick, Phylo.path("hummingbirds.tree")) + 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(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 + @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 From ffb5520fd6e371fa0aa4e9584088f98b05a164be Mon Sep 17 00:00:00 2001 From: richardreeve Date: Sat, 2 Dec 2023 04:00:17 +0000 Subject: [PATCH 06/22] Mistaken version number --- .github/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index ee5154ef..0d7f2e7f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -16,7 +16,7 @@ jobs: matrix: julia-version: - '1.0' - - 'lts' + - '1.6' - '1' os: - ubuntu-latest From eed50dae87fc89cdb9b67adb25cffebb9b2f43ba Mon Sep 17 00:00:00 2001 From: richardreeve Date: Mon, 4 Dec 2023 05:06:48 +0000 Subject: [PATCH 07/22] Add in fully recursive tree type --- src/API.jl | 17 +- src/Interface.jl | 37 +-- src/LinkTree.jl | 6 - src/Phylo.jl | 15 +- src/RecursiveTree.jl | 663 +++++++++++++++++++++++++++++++++++++++++ src/newick.jl | 67 ++++- src/plot.jl | 8 +- src/routes.jl | 16 +- test/run_rcall.jl | 3 +- test/test_Interface.jl | 1 + test/test_newick.jl | 2 +- test/test_plot.jl | 2 +- test/test_rand.jl | 2 +- test/test_routes.jl | 3 +- test/test_show.jl | 2 +- 15 files changed, 782 insertions(+), 62 deletions(-) create mode 100644 src/RecursiveTree.jl diff --git a/src/API.jl b/src/API.jl index fd80d99f..617d5f06 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) @@ -644,7 +641,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 +748,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 +831,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..802d4cc7 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}) 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..834e9522 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,14 @@ 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 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 @@ -149,6 +153,9 @@ export PolytomousTree, NamedPolytomousTree include("LinkTree.jl") export LinkBranch, LinkNode, LinkTree + +include("RecursiveTree.jl") +export RecursiveElt, RecursiveBranch, RecursiveNode, RecursiveTree export RootedTree, ManyRootTree, UnrootedTree include("routes.jl") diff --git a/src/RecursiveTree.jl b/src/RecursiveTree.jl new file mode 100644 index 00000000..b01e854a --- /dev/null +++ b/src/RecursiveTree.jl @@ -0,0 +1,663 @@ +using Unitful + +""" + struct RecursiveElt <: AbstractElt + +A type for branches or nodes in a RecursiveTree +""" +Base.@kwdef mutable struct RecursiveElt{RT, NL, MyType <: AbstractElt{RT, NL}, MyData, + TheirType <: AbstractElt{RT, NL}, TheirData, + LenUnits <: Number} <: AbstractElt{RT, NL} + name::Union{NL, Nothing} = nothing + id::Union{Int, Missing} = missing + in::Union{RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, + LenUnits}, Nothing} = nothing + conns::Vector{RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, + LenUnits}} = + RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, LenUnits}[] + data::MyData = _emptydata(MyData) + length::Union{LenUnits, Missing} = missing +end + +const RecursiveNode{RT, NL, NodeData, BranchData, LenUnits} = + RecursiveElt{RT, NL, AbstractNode{RT, NL}, NodeData, + AbstractBranch{RT, NL}, BranchData, LenUnits} + +const RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits} = + RecursiveElt{RT, NL, AbstractBranch{RT, NL}, BranchData, + AbstractNode{RT, NL}, NodeData, LenUnits} + +RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}(name::NL, data::NodeData = nothing) where + {RT, NL, NodeData, BranchData, LenUnits} = + RecursiveNode{RT, NL, NodeData, BranchData, + LenUnits}(name, nothing, + RecursiveBranch{RT, NL, NodeData, + BranchData, LenUnits}[], data, missing) + +import Phylo.API: _prefernodeobjects, _preferbranchobjects +_prefernodeobjects(::Type{<:RecursiveElt}) = true +_preferbranchobjects(::Type{<:RecursiveElt}) = true + +""" + struct RecoursiveTree <: AbstractTree + +A phylogenetic tree type containing RecursiveElts as nodes and branches +""" +mutable struct RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD} <: + AbstractTree{OneTree, RT, NL, + RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}} + + name::String + nodedict::Dict{NL, Int} + roots::Vector{RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}} + nodes::Vector{Union{RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + Missing}} + branches::Vector{Union{RecursiveBranch{RT, NL, NodeData, BranchData, 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, LenUnits}}} + + function RecursiveTree{RT, NL, NodeData, BranchData, + 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, LenUnits, TD} + NT = RecursiveNode{RT, NL, NodeData, BranchData, LenUnits} + BT = RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits} + tree = new{RT, NL, NodeData, BranchData, + LenUnits, TD}(name, Dict{NL, NT}(), NT[], + Union{NT, Missing}[], Union{BT, 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 + +import Phylo.API: _validate! +function _validate!(tree::RecursiveTree{RT, NL, NodeData, BranchData, + LenUnits, TD}) where + {RT, NL, NodeData, BranchData, 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 + 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 + +# Type aliases + +const ReB{RT, LenUnits} = RecursiveBranch{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits} +const ReN{RT, LenUnits} = RecursiveNode{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits} +const ReT{RT, TD, LenUnits} = RecursiveTree{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits, TD} +const ReTD{RT, LenUnits} = ReT{RT, Dict{String, Any}, LenUnits} +const RootedTree = ReTD{OneRoot, Float64} +const ManyRootTree = ReTD{ManyRoots, Float64} +const UnrootedTree = ReTD{Unrooted, Float64} + +# Types matches + +_emptydata(::Type{Data}) where Data = Data() +_emptydata(::Type{RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}}) where + {RT, NL, NodeData, BranchData, LenUnits} = _newdata(NodeData) +_emptydata(::Type{RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}}) where + {RT, NL, NodeData, BranchData, LenUnits} = _newdata(BranchData) + +_tiplabeltype(::Type{Nothing}) = Nothing +_tiplabeltype(::Type{<: Dict{NL}}) where NL = NL +_tiplabeltype(::Type{RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, 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, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, LenUnits, TD} = TD + +import Phylo.API: _getleafinfo +_getleafinfo(tree::RecursiveTree) = tree.tipdata + +import Phylo.API: _setleafinfo! +_setleafinfo!(tree::RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD}, leafinfo::TD) where + {RT, NL, NodeData, BranchData, 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, NodeData, BranchData, LenUnits}, node::N) where + {RT, NL, NodeData, BranchData, LenUnits, + N <: RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}} = + 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, LenUnits}, + name::Union{NL, Missing}, data::NodeData = _emptydata(NodeData)) where + {RT <: Rooted, NL, NodeData, BranchData, LenUnits} + + NT = RecursiveNode{RT, NL, NodeData, BranchData, 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) + + tree.isvalid = missing + return node +end + +function _createnode!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, LenUnits}, + name::Union{NL, Missing}, data::NodeData = _emptydata(NodeData)) where + {NL, NodeData, BranchData, LenUnits} + + NT = RecursiveNode{Unrooted, NL, NodeData, BranchData, 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 + + tree.isvalid = missing + 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) + tree.isvalid = missing + 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) + tree.isvalid = missing +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) + tree.isvalid = missing +end + +import Phylo.API: _addoutbound! +function _addoutbound!(tree::RecursiveTree{RT}, + node::RecursiveNode{RT}, + branch::RecursiveBranch{RT}) where RT <: Rooted + push!(node.conns, branch) + tree.isvalid = missing +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) + tree.isvalid = missing +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) + tree.isvalid = missing +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) + tree.isvalid = missing +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, LenUnits}}) where + {RT, NL, NodeData, BranchData, 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, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, LenUnits, TD} = NodeData +_nodedatatype(::Type{<:RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}}) where + {RT, NL, NodeData, BranchData, LenUnits} = NodeData + +import Phylo.API: _getnodedata +_getnodedata(::RecursiveTree, node::RecursiveNode) = node.data + +import Phylo.API: _setnodedata! +_setnodedata!(::RecursiveTree{RT, NL, NodeData, BranchData, LenUnits}, + node::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + data::NodeData) where {RT, NL, NodeData, BranchData, LenUnits} = (node.data = data) + +# Information about individual branches stored on the branches + +import Phylo.API: _branchdatatype +_branchdatatype(::Type{<:RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD}}) where + {RT, NL, NodeData, BranchData, LenUnits, TD} = BranchData +_branchdatatype(::Type{<:RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}}) where + {RT, NL, NodeData, BranchData, LenUnits} = BranchData + +import Phylo.API: _getbranchdata +_getbranchdata(::RecursiveTree, branch::RecursiveBranch) = branch.data + +import Phylo.API: _setbranchdata! +_setbranchdata!(::RecursiveTree{RT, NL, NodeData, BranchData, LenUnits}, + branch::RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}, + data::BranchData) where {RT, NL, NodeData, BranchData, LenUnits} = + (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, LenUnits}, + from::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + to::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + len::Union{LenUnits, Missing} = missing, + name = missing, + data::BranchData = _emptydata(BranchData)) where + {RT <: Rooted, NL, NodeData, BranchData, LenUnits} + + 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 $(from.in.name) already has an inbound connection") + + _hasoutboundspace(tree, from) || + error("Node $(to.conns[1].name) already has an outbound connection") + + branch = RecursiveBranch{RT, NL, NodeData, BranchData, + 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) + + tree.isvalid = missing + return branch +end + +function _createbranch!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, LenUnits}, + from::RecursiveNode{Unrooted, NL, NodeData, BranchData, LenUnits}, + to::RecursiveNode{Unrooted, NL, NodeData, BranchData, LenUnits}, + len::Union{LenUnits, Missing} = missing, + name = missing, + data::BranchData = _emptydata(BranchData)) where + {Unrooted, NL, NodeData, BranchData, LenUnits} + + 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, + LenUnits}(id = name, conns = [to, from], + length = len, data = data) + + _hasspace(tree, from) || + error("Node $(from.name) has no space from new connections") + + _hasspace(tree, to) || + error("Node $(to.name) has no space from 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) + + tree.isvalid = missing + 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 + tree.isvalid = missing + return true +end + +function _deletebranch!(tree::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}) + _removeconnection!(tree, branch.in, branch) + _removeconnection!(tree, branch.conns[1], branch) + tree.branches[branch.id] = missing + tree.isvalid = missing + 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..fb1780d6 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 @@ -293,7 +358,7 @@ function parsenode(token, state, tokens, tree::TREE, token, state = result end - return token, state, myname + return token, state, getnodename(tree, myname) end function parsenewick!(token, state, tokens, tree::TREE, diff --git a/src/plot.jl b/src/plot.jl index 542a9984..d7604d64 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 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_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 c5b083c6..6083dc99 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -9,7 +9,7 @@ using Random 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(tree, treetype = :fan, line_z = trait, linecolor = :RdYlBu, linewidth = 5, showtips = false).n == 1 + @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, 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..ce1ffd04 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) 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) From 90548de3e6a2114dd2e2f47b82b40a76af88e0e6 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Mon, 4 Dec 2023 05:29:23 +0000 Subject: [PATCH 08/22] Going back to Julia 1.6 --- Project.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 0c1f4452..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.25" +version = "0.5.0" [deps] AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" @@ -42,17 +42,17 @@ IterableTables = "1" Missings = "1" ParserCombinator = "2" Plots = "1" -Printf = "<0.0.1, 1" +Printf = "1.6" RCall = "0.13" -Random = "<0.0.1, 1" +Random = "1.6" RecipesBase = "1" Requires = "1" SimpleTraits = "0.9" -Statistics = "<0.0.1, 1" -Test = "<0.0.1, 1" +Statistics = "1.6" +Test = "1.6" Tokenize = "0.5" Unitful = "1" -julia = "1" +julia = "1.6" [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" From 18b05526cd32345b8cffc71487707ea41c2c49eb Mon Sep 17 00:00:00 2001 From: richardreeve Date: Mon, 4 Dec 2023 05:31:28 +0000 Subject: [PATCH 09/22] Remove Julia 1.0 testing and use isnothing() --- .github/workflows/testing.yaml | 1 - src/Iterators.jl | 38 +++++------ src/newick.jl | 118 ++++++++++++++++----------------- src/plot.jl | 14 ++-- 4 files changed, 85 insertions(+), 86 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0d7f2e7f..0c8771ff 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -15,7 +15,6 @@ jobs: strategy: matrix: julia-version: - - '1.0' - '1.6' - '1' os: 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/newick.jl b/src/newick.jl index fb1780d6..401ee793 100644 --- a/src/newick.jl +++ b/src/newick.jl @@ -88,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) @@ -117,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) @@ -128,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, ";") @@ -142,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 @@ -168,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 @@ -197,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 @@ -214,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 @@ -228,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) @@ -237,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 @@ -259,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 @@ -273,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 @@ -318,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 @@ -332,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] @@ -354,7 +354,7 @@ 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 @@ -368,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 @@ -380,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 @@ -398,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 ';'") @@ -414,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 @@ -475,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) @@ -506,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 @@ -525,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;") @@ -538,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) @@ -560,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 @@ -574,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]) @@ -593,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) @@ -603,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 @@ -612,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 @@ -626,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 @@ -673,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 d7604d64..3b85cd3d 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -15,8 +15,8 @@ using RecipesBase lz = get(plotattributes, :line_z, nothing) mz = get(plotattributes, :marker_z, nothing) - lz === nothing || (line_z := _handlez(lz, tree, n)) - mz === nothing || (marker_z := _handlez(mz, tree, n)) + isnothing(lz) || (line_z := _handlez(lz, tree, n)) + isnothing(mz) || (marker_z := _handlez(mz, tree, n)) mg = _handlez(marker_group, tree, n) lg = _handlez(line_group, tree, n) @@ -68,7 +68,7 @@ struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; marker_ dend.x, dend.y end if !isempty(dend.marker_x) || sa !== nothing - if dend.marker_group === nothing + if isnothing(dend.marker_group) @series begin seriestype := :scatter sa !== nothing && (series_annotations := sa) @@ -125,7 +125,7 @@ end x, y end if !isempty(fan.marker_x) || sa !== nothing - if fan.marker_group === nothing + if isnothing(fan.marker_group) @series begin seriestype := :scatter sa !== nothing && (series_annotations := sa) @@ -167,7 +167,7 @@ end _mylength(x) = 1 _mylength(x::AbstractVector) = length(x) function _handlemarkers(plotattributes, marker_group, tree, d, h, names) - marker_group === nothing || (plotattributes[:marker_group] = marker_group) + isnothing(marker_group) || (plotattributes[:marker_group] = marker_group) markerfields = filter(x->occursin(r"marker", String(x)), keys(plotattributes)) isempty(markerfields) && return (Float64[], Float64[]) maxlength = maximum([_mylength(plotattributes[k]) for k in markerfields]) @@ -286,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 @@ -311,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) From e46f5371038c32770d5b6c51dd1ecd10f1f8a142 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Mon, 4 Dec 2023 12:25:59 +0000 Subject: [PATCH 10/22] Include missing docs for Recursive tree and node/branch types --- docs/src/man/treetypes.md | 9 +++------ src/RecursiveTree.jl | 6 ++++-- 2 files changed, 7 insertions(+), 8 deletions(-) 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/RecursiveTree.jl b/src/RecursiveTree.jl index b01e854a..6c8baa8c 100644 --- a/src/RecursiveTree.jl +++ b/src/RecursiveTree.jl @@ -3,7 +3,8 @@ using Unitful """ struct RecursiveElt <: AbstractElt -A type for branches or nodes in a RecursiveTree +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, @@ -41,7 +42,8 @@ _preferbranchobjects(::Type{<:RecursiveElt}) = true """ struct RecoursiveTree <: AbstractTree -A phylogenetic tree type containing RecursiveElts as nodes and branches +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, LenUnits, TD} <: AbstractTree{OneTree, RT, NL, From eece18b0553da22e72d35b4cb42231e63f384576 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 00:06:18 +0000 Subject: [PATCH 11/22] Allow for binary and polytomous branching in trees --- test/test_LinkTree.jl | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) 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) From 77c2e4dd3ad3d5ee090a8ef6d9e74e8cb266a6dd Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 00:08:13 +0000 Subject: [PATCH 12/22] Fix binary and polytomous RecursiveTrees --- src/API.jl | 11 +- src/Interface.jl | 7 ++ src/Phylo.jl | 11 +- src/RecursiveTree.jl | 268 +++++++++++++++++++++++++------------------ 4 files changed, 181 insertions(+), 116 deletions(-) diff --git a/src/API.jl b/src/API.jl index 617d5f06..594e1b73 100644 --- a/src/API.jl +++ b/src/API.jl @@ -512,11 +512,18 @@ function _clearrootheight! end """ _validate!(::AbstractTree) - +Check whether the tree is internally valid. """ function _validate! end _validate!(::AbstractTree) = true +""" + _invalidate!(::AbstractTree) + +Confirm that the tree is no longer necessarily valid, and remove cache information. +""" +function _invalidate! end + """ _traversal(tree::AbstractTree, order::TraversalOrder, todo, sofar) @@ -629,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) """ diff --git a/src/Interface.jl b/src/Interface.jl index 802d4cc7..76436137 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -1228,6 +1228,13 @@ function validate!(tree::T) where return _validate!(tree) end +""" + invalidate!(tree::AbstractTree) + +Invalidate the tree. +""" +invalidate!(tree::AbstractTree) = _invalidate!(tree) + """ traversal(::AbstractTree, ::TraversalOrder) traversal(::AbstractTree, ::TraversalOrder, init) diff --git a/src/Phylo.jl b/src/Phylo.jl index 834e9522..ba72e3f2 100644 --- a/src/Phylo.jl +++ b/src/Phylo.jl @@ -35,6 +35,11 @@ struct OneTree <: TreeType end struct ManyTrees <: TreeType end export OneTree, ManyTrees +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 @@ -72,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 @@ -106,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! @@ -156,7 +161,7 @@ export LinkBranch, LinkNode, LinkTree include("RecursiveTree.jl") export RecursiveElt, RecursiveBranch, RecursiveNode, RecursiveTree -export RootedTree, ManyRootTree, UnrootedTree +export RootedTree, ManyRootTree, UnrootedTree, BinaryRootedTree include("routes.jl") export branchhistory, branchfuture, branchroute diff --git a/src/RecursiveTree.jl b/src/RecursiveTree.jl index 6c8baa8c..c67e3401 100644 --- a/src/RecursiveTree.jl +++ b/src/RecursiveTree.jl @@ -8,36 +8,35 @@ using the tree object itself. """ Base.@kwdef mutable struct RecursiveElt{RT, NL, MyType <: AbstractElt{RT, NL}, MyData, TheirType <: AbstractElt{RT, NL}, TheirData, - LenUnits <: Number} <: AbstractElt{RT, NL} + 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, - LenUnits}, Nothing} = nothing + BT, LenUnits}, Nothing} = nothing conns::Vector{RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, - LenUnits}} = - RecursiveElt{RT, NL, TheirType, TheirData, MyType, MyData, LenUnits}[] + 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, LenUnits} = +const RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits} = RecursiveElt{RT, NL, AbstractNode{RT, NL}, NodeData, - AbstractBranch{RT, NL}, BranchData, LenUnits} + AbstractBranch{RT, NL}, BranchData, BT, LenUnits} -const RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits} = - RecursiveElt{RT, NL, AbstractBranch{RT, NL}, BranchData, - AbstractNode{RT, NL}, NodeData, 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, LenUnits}(name::NL, data::NodeData = nothing) where - {RT, NL, NodeData, BranchData, 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, - LenUnits}(name, nothing, - RecursiveBranch{RT, NL, NodeData, - BranchData, LenUnits}[], data, missing) - -import Phylo.API: _prefernodeobjects, _preferbranchobjects -_prefernodeobjects(::Type{<:RecursiveElt}) = true -_preferbranchobjects(::Type{<:RecursiveElt}) = true + BT, LenUnits}(name, nothing, + RecursiveBranch{RT, NL, NodeData, + BranchData, LenUnits}[], + data, missing) """ struct RecoursiveTree <: AbstractTree @@ -45,40 +44,40 @@ _preferbranchobjects(::Type{<:RecursiveElt}) = true 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, LenUnits, TD} <: +mutable struct RecursiveTree{RT, NL, NodeData, BranchData, BT <: BranchingType, LenUnits, TD} <: AbstractTree{OneTree, RT, NL, - RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, - RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}} + 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, LenUnits}} - nodes::Vector{Union{RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, + 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, LenUnits}, + 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, LenUnits}}} + Vector{RecursiveNode{RT, NL, NodeData, BranchData, BT, LenUnits}}} function RecursiveTree{RT, NL, NodeData, BranchData, - 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, LenUnits, TD} - NT = RecursiveNode{RT, NL, NodeData, BranchData, LenUnits} - BT = RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits} + 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, - LenUnits, TD}(name, Dict{NL, NT}(), NT[], - Union{NT, Missing}[], Union{BT, Missing}[], - Dict{String, Any}(), - tipdata, rootheight, missing, - Dict{TraversalOrder, Vector{NT}}()) + 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) @@ -96,10 +95,25 @@ mutable struct RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD} <: 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, - LenUnits, TD}) where - {RT, NL, NodeData, BranchData, LenUnits, TD} + BT, LenUnits, TD}) where + {RT, NL, NodeData, BranchData, BT, LenUnits, TD} tree.isvalid = true @@ -151,6 +165,15 @@ function _validate!(tree::RecursiveTree{RT, NL, NodeData, BranchData, @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 @@ -191,28 +214,35 @@ function _validate!(tree::RecursiveTree{RT, NL, NodeData, BranchData, 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, LenUnits} = RecursiveBranch{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits} -const ReN{RT, LenUnits} = RecursiveNode{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits} -const ReT{RT, TD, LenUnits} = RecursiveTree{RT, String, Dict{String, Any}, Dict{String, Any}, LenUnits, TD} -const ReTD{RT, LenUnits} = ReT{RT, Dict{String, Any}, LenUnits} -const RootedTree = ReTD{OneRoot, Float64} -const ManyRootTree = ReTD{ManyRoots, Float64} -const UnrootedTree = ReTD{Unrooted, Float64} +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, LenUnits}}) where - {RT, NL, NodeData, BranchData, LenUnits} = _newdata(NodeData) -_emptydata(::Type{RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}}) where - {RT, NL, NodeData, BranchData, LenUnits} = _newdata(BranchData) +_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, LenUnits, TD}}) where - {RT, NL, NodeData, BranchData, LenUnits, TD} = _tiplabeltype(TD) +_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 @@ -229,15 +259,15 @@ _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, LenUnits, TD}}) where - {RT, NL, NodeData, BranchData, LenUnits, TD} = TD +_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, LenUnits, TD}, leafinfo::TD) where - {RT, NL, NodeData, BranchData, LenUnits, TD} = (tree.tipdata = leafinfo) +_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 @@ -257,9 +287,8 @@ _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, NodeData, BranchData, LenUnits}, node::N) where - {RT, NL, NodeData, BranchData, LenUnits, - N <: RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}} = +_hasnode(tree::RecursiveTree{RT, NL}, node::N) where + {RT, NL, N <: RecursiveNode{RT, NL}} = haskey(tree.nodedict, node.name) import Phylo.API: _getnode @@ -273,11 +302,11 @@ _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, LenUnits}, +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, LenUnits} + {RT <: Rooted, NL, NodeData, BranchData, BT, LenUnits, TD} - NT = RecursiveNode{RT, NL, NodeData, BranchData, LenUnits} + 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 @@ -286,15 +315,15 @@ function _createnode!(tree::RecursiveTree{RT, NL, NodeData, BranchData, LenUnits tree.nodedict[nodename] = id push!(tree.roots, node) - tree.isvalid = missing + _invalidate!(tree) return node end -function _createnode!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, LenUnits}, +function _createnode!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, BT, LenUnits, TD}, name::Union{NL, Missing}, data::NodeData = _emptydata(NodeData)) where - {NL, NodeData, BranchData, LenUnits} + {NL, NodeData, BranchData, BT, LenUnits, TD} - NT = RecursiveNode{Unrooted, NL, NodeData, BranchData, LenUnits} + 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 @@ -302,7 +331,7 @@ function _createnode!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, Le push!(tree.nodes, node) tree.nodedict[nodename] = id - tree.isvalid = missing + _invalidate!(tree) return node end @@ -324,7 +353,7 @@ function _deletenode!(tree::RecursiveTree, node::RecursiveNode) tree.nodes[tree.nodedict[node.name]] = missing delete!(tree.nodedict, node.name) filter!(n -> n ≢ node, tree.roots) - tree.isvalid = missing + _invalidate!(tree) return true end @@ -353,7 +382,7 @@ function _addinbound!(tree::RecursiveTree{RT}, error("RecursiveNode $(node.name) already has an inbound connection") node.in = branch filter!(n -> n ≠ node, tree.roots) - tree.isvalid = missing + _invalidate!(tree) end import Phylo.API: _removeinbound! @@ -365,15 +394,32 @@ function _removeinbound!(tree::RecursiveTree{RT}, error("Node $(node.name) has no inbound connection from branch $(branch.id)") node.in = nothing push!(tree.roots, node) - tree.isvalid = missing + _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) - tree.isvalid = missing + _invalidate!(tree) end import Phylo.API: _removeoutbound! @@ -384,7 +430,7 @@ function _removeoutbound!(tree::RecursiveTree{RT}, error("Node $(node.name) does not have outbound connection to branch $(branch.id)") end filter!(p -> p ≢ branch, node.conns) - tree.isvalid = missing + _invalidate!(tree) end import Phylo.API: _addconnection! @@ -395,7 +441,7 @@ function _addconnection!(tree::RecursiveTree{Unrooted}, error("Node $(node.name) does not have space for a new connection") end push!(node.conns, branch) - tree.isvalid = missing + _invalidate!(tree) end import Phylo.API: _removeconnection! @@ -406,7 +452,7 @@ function _removeconnection!(tree::RecursiveTree{Unrooted}, error("Node $(node.name) does not have connection to branch $(branch.id)") end filter!(p -> p ≢ branch, node.conns) - tree.isvalid = missing + _invalidate!(tree) end # Retrieving branches @@ -436,8 +482,8 @@ _getbranchname(::RecursiveTree, branch::RecursiveBranch) = branch.id import Phylo.API: _branchdims _branchdims(T::Type{<:RecursiveTree}) = _branchdims(branchtype(T)) -_branchdims(::Type{<:RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}}) where - {RT, NL, NodeData, BranchData, LenUnits} = dimension(LenUnits) +_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 @@ -463,34 +509,34 @@ _conns(::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}) = branch.co # Information about individual nodes stored on the nodes import Phylo.API: _nodedatatype -_nodedatatype(::Type{<:RecursiveTree{RT, NL, NodeData, BranchData, LenUnits, TD}}) where - {RT, NL, NodeData, BranchData, LenUnits, TD} = NodeData -_nodedatatype(::Type{<:RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}}) where - {RT, NL, NodeData, BranchData, LenUnits} = NodeData +_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, LenUnits}, - node::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, - data::NodeData) where {RT, NL, NodeData, BranchData, LenUnits} = (node.data = data) +_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, LenUnits, TD}}) where - {RT, NL, NodeData, BranchData, LenUnits, TD} = BranchData -_branchdatatype(::Type{<:RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}}) where - {RT, NL, NodeData, BranchData, LenUnits} = BranchData +_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, LenUnits}, - branch::RecursiveBranch{RT, NL, NodeData, BranchData, LenUnits}, - data::BranchData) where {RT, NL, NodeData, BranchData, LenUnits} = +_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 @@ -507,13 +553,13 @@ import Phylo.API: _newbranchlabel _newbranchlabel(tree::RecursiveTree) = length(tree.branches) + 1 import Phylo.API: _createbranch! -function _createbranch!(tree::RecursiveTree{RT, NL, NodeData, BranchData, LenUnits}, - from::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, - to::RecursiveNode{RT, NL, NodeData, BranchData, LenUnits}, +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, LenUnits} + {RT <: Rooted, NL, NodeData, BranchData, BT, LenUnits, TD} if ismissing(name) name = length(tree.branches) + 1 @@ -523,14 +569,14 @@ function _createbranch!(tree::RecursiveTree{RT, NL, NodeData, BranchData, LenUni error("Branch length must be positive or missing (no recorded length), not $len") _hasinboundspace(tree, to) || - error("Node $(from.in.name) already has an inbound connection") + error("Node $(to.name) already has an inbound connection") _hasoutboundspace(tree, from) || - error("Node $(to.conns[1].name) already has an outbound connection") + error("Node $(from.name) has no outbound space") branch = RecursiveBranch{RT, NL, NodeData, BranchData, - LenUnits}(id = name, in = from, conns = [to], - length = len, data = data) + BT, LenUnits}(id = name, in = from, conns = [to], + length = len, data = data) if 1 ≤ name ≤ length(tree.branches) tree.branches[name] = branch @@ -548,17 +594,17 @@ function _createbranch!(tree::RecursiveTree{RT, NL, NodeData, BranchData, LenUni _addoutbound!(tree, from, branch) _addinbound!(tree, to, branch) - tree.isvalid = missing + _invalidate!(tree) return branch end -function _createbranch!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, LenUnits}, - from::RecursiveNode{Unrooted, NL, NodeData, BranchData, LenUnits}, - to::RecursiveNode{Unrooted, NL, NodeData, BranchData, LenUnits}, +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, LenUnits} + {Unrooted, NL, NodeData, BranchData, BT, LenUnits, TD} if ismissing(name) name = length(tree.branches) + 1 @@ -568,14 +614,14 @@ function _createbranch!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, error("Branch length must be positive or missing (no recorded length), not $length") branch = RecursiveBranch{Unrooted, NL, NodeData, BranchData, - LenUnits}(id = name, conns = [to, from], - length = len, data = data) + BT, LenUnits}(id = name, conns = [to, from], + length = len, data = data) _hasspace(tree, from) || - error("Node $(from.name) has no space from new connections") + error("Node $(from.name) has no space for new connections") _hasspace(tree, to) || - error("Node $(to.name) has no space from new connections") + error("Node $(to.name) has no space for new connections") if 1 ≤ name ≤ length(tree.branches) tree.branches[name] = branch @@ -593,7 +639,7 @@ function _createbranch!(tree::RecursiveTree{Unrooted, NL, NodeData, BranchData, _addconnection!(tree, from, branch) _addconnection!(tree, to, branch) - tree.isvalid = missing + _invalidate!(tree) return branch end @@ -601,7 +647,7 @@ function _deletebranch!(tree::RecursiveTree{RT}, branch::RecursiveBranch{RT}) wh _removeoutbound!(tree, branch.in, branch) _removeinbound!(tree, branch.conns[1], branch) tree.branches[branch.id] = missing - tree.isvalid = missing + _invalidate!(tree) return true end @@ -609,7 +655,7 @@ function _deletebranch!(tree::RecursiveTree{Unrooted}, branch::RecursiveBranch{U _removeconnection!(tree, branch.in, branch) _removeconnection!(tree, branch.conns[1], branch) tree.branches[branch.id] = missing - tree.isvalid = missing + _invalidate!(tree) return true end From 8ff9cf13f8ecfdbac59b2c1e0cc01e8450b9381d Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 00:13:29 +0000 Subject: [PATCH 13/22] Add testing for RecursiveTrees --- test/test_RecursiveTree.jl | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/test_RecursiveTree.jl diff --git a/test/test_RecursiveTree.jl b/test/test_RecursiveTree.jl new file mode 100644 index 00000000..62a87da2 --- /dev/null +++ b/test/test_RecursiveTree.jl @@ -0,0 +1,76 @@ +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 "maximum number" 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) + @test createbranch!(rtdfp, name, species[3]) ∈ getbranches(rtdfp) + @test_throws "does not have an available destination node" 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 "maximum number" 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) + @test createbranch!(rtjdb, name, observations[1], data = nothing) ∈ getbranches(rtjdb) +end + +end From a4f69aeb1d0aa30c63f49bb726cff4c61baf8ea2 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 00:17:36 +0000 Subject: [PATCH 14/22] Fix docs --- docs/src/man/attributes.md | 1 + src/API.jl | 2 +- src/Interface.jl | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) 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/src/API.jl b/src/API.jl index 594e1b73..ca388eb9 100644 --- a/src/API.jl +++ b/src/API.jl @@ -518,7 +518,7 @@ function _validate! end _validate!(::AbstractTree) = true """ - _invalidate!(::AbstractTree) + _invalidate!(::AbstractTree, state) Confirm that the tree is no longer necessarily valid, and remove cache information. """ diff --git a/src/Interface.jl b/src/Interface.jl index 76436137..c8144018 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -1229,11 +1229,11 @@ function validate!(tree::T) where end """ - invalidate!(tree::AbstractTree) + invalidate!(tree::AbstractTree, state = missing) -Invalidate the tree. +Confirm that the tree is no longer necessarily valid, and remove cache information. """ -invalidate!(tree::AbstractTree) = _invalidate!(tree) +invalidate!(tree::AbstractTree, state = missing) = _invalidate!(tree, state) """ traversal(::AbstractTree, ::TraversalOrder) From e32895f1113b199f64d3d7cfae2852e52a63fd74 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 00:36:42 +0000 Subject: [PATCH 15/22] Fix thrown error testing for Julia 1.6 --- test/test_RecursiveTree.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_RecursiveTree.jl b/test/test_RecursiveTree.jl index 62a87da2..7acc5ece 100644 --- a/test/test_RecursiveTree.jl +++ b/test/test_RecursiveTree.jl @@ -28,7 +28,7 @@ jdb = DataFrame(species = observations, count = 1:4) @test_nowarn createnode!(rtdf, name) @test createbranch!(rtdf, name, species[1]) ∈ getbranches(rtdf) @test createbranch!(rtdf, name, species[2]) ∈ getbranches(rtdf) - @test_throws "maximum number" createbranch!(rtdf, name, species[3]) + @test_throws ErrorException createbranch!(rtdf, name, species[3]) rtdfp = Phylo.ReT{OneRoot, DataFrame, PolytomousBranching, Float64}(df) @test nodedatatype(typeof(rtdfp)) ≡ Dict{String, Any} @@ -38,7 +38,7 @@ jdb = DataFrame(species = observations, count = 1:4) @test createbranch!(rtdfp, name, species[1]) ∈ getbranches(rtdfp) @test createbranch!(rtdfp, name, species[2]) ∈ getbranches(rtdfp) @test createbranch!(rtdfp, name, species[3]) ∈ getbranches(rtdfp) - @test_throws "does not have an available destination node" createbranch!(rtdfp, name, species[2]) + @test_throws ErrorException createbranch!(rtdfp, name, species[2]) end @testset "UnrootedTree()" begin @@ -51,7 +51,7 @@ end @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 "maximum number" createbranch!(urts, name, species[4]) + @test_throws ErrorException createbranch!(urts, name, species[4]) urtsp = UnrootedTree(species) @test nodedatatype(typeof(urtsp)) ≡ Dict{String, Any} From 4f7d40f4f499109226a4f4e0268a87cd144ff60a Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:00:52 +0000 Subject: [PATCH 16/22] More route testing --- test/test_routes.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_routes.jl b/test/test_routes.jl index ce1ffd04..ac0873df 100644 --- a/test/test_routes.jl +++ b/test/test_routes.jl @@ -49,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)) From d46c808066a14c52876628888e112ab67e7e1531 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:10:15 +0000 Subject: [PATCH 17/22] Fix plot test --- test/test_plot.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_plot.jl b/test/test_plot.jl index 6083dc99..fdd2547f 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -12,8 +12,8 @@ using Random @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 + 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") From c7abf7080941781536f83fb47cd23050ef353f45 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:25:00 +0000 Subject: [PATCH 18/22] Fix plottest and docs --- test/test_plot.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_plot.jl b/test/test_plot.jl index fdd2547f..60f61de2 100644 --- a/test/test_plot.jl +++ b/test/test_plot.jl @@ -13,7 +13,7 @@ using Random @test plot(tree, markersize = 10, markercolor = :steelblue, markerstrokecolor = :white, series_annotations = text.(1:nnodes(tree), 5, :center, :center, :white), - tipfont = (4,)).init + tipfont = (4,)).init @enum TemperatureTrait lowTempPref midTempPref highTempPref tempsampler = SymmetricDiscreteTrait(tree, TemperatureTrait, 0.4, "Temperature") From 306c4364381dd3febba97d9388d22eba13c1e2a1 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:25:27 +0000 Subject: [PATCH 19/22] Fix plot docs --- docs/src/man/plotting.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 From a4c1295a6cb484fd6d5c0b44c2a153a8041f1429 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:37:44 +0000 Subject: [PATCH 20/22] Test deleting branches and fix unrooted error --- src/RecursiveTree.jl | 2 +- test/test_RecursiveTree.jl | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/RecursiveTree.jl b/src/RecursiveTree.jl index c67e3401..5a97c411 100644 --- a/src/RecursiveTree.jl +++ b/src/RecursiveTree.jl @@ -652,8 +652,8 @@ function _deletebranch!(tree::RecursiveTree{RT}, branch::RecursiveBranch{RT}) wh end function _deletebranch!(tree::RecursiveTree{Unrooted}, branch::RecursiveBranch{Unrooted}) - _removeconnection!(tree, branch.in, branch) _removeconnection!(tree, branch.conns[1], branch) + _removeconnection!(tree, branch.conns[2], branch) tree.branches[branch.id] = missing _invalidate!(tree) return true diff --git a/test/test_RecursiveTree.jl b/test/test_RecursiveTree.jl index 7acc5ece..4bc6b92c 100644 --- a/test/test_RecursiveTree.jl +++ b/test/test_RecursiveTree.jl @@ -37,6 +37,9 @@ jdb = DataFrame(species = observations, count = 1:4) @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 @@ -70,7 +73,11 @@ end @test branchdatatype(typeof(rtjdb)) ≡ Nothing @test leafinfotype(typeof(rtjdb)) ≡ typeof(jdb) @test_nowarn createnode!(rtjdb, name) - @test createbranch!(rtjdb, name, observations[1], data = nothing) ∈ getbranches(rtjdb) + 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(rtdfp) + end end From 226dc091aaf077279814e049e1554ede6d761e74 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Wed, 6 Dec 2023 01:37:54 +0000 Subject: [PATCH 21/22] Fix test --- test/test_RecursiveTree.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_RecursiveTree.jl b/test/test_RecursiveTree.jl index 4bc6b92c..4a8bd9a0 100644 --- a/test/test_RecursiveTree.jl +++ b/test/test_RecursiveTree.jl @@ -76,7 +76,7 @@ end 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(rtdfp) + @test createbranch!(rtjdb, name, observations[1], data = nothing) ∈ getbranches(rtjdb) end From 6f1b300b66f98d5e7b2a37233f757d25b05ad6f8 Mon Sep 17 00:00:00 2001 From: richardreeve Date: Thu, 7 Dec 2023 10:16:54 +0000 Subject: [PATCH 22/22] Update NEWS.md for new release --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) 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