diff --git a/.github/workflows/doc-cleanup.yaml b/.github/workflows/doc-cleanup.yaml index 5945be87..a63e571d 100644 --- a/.github/workflows/doc-cleanup.yaml +++ b/.github/workflows/doc-cleanup.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout gh-pages branch - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: gh-pages - name: Delete preview and history + push changes diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2fd032ef..a53eb5ec 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up julia uses: julia-actions/setup-julia@v1 with: diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 43c6d152..426d65f5 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -15,13 +15,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up julia uses: julia-actions/setup-julia@v1 with: version: nightly arch: x64 - name: Build package - uses: julia-actions/julia-buildpkg@latest + uses: julia-actions/julia-buildpkg@v1 - name: Run tests - uses: julia-actions/julia-runtest@latest + uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 323ec5df..0c8771ff 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -30,7 +30,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up julia uses: julia-actions/setup-julia@v1 with: @@ -47,15 +47,10 @@ jobs: install.packages("ape", repos="http://cran.r-project.org") shell: R --vanilla --file={0} - name: Build package - uses: julia-actions/julia-buildpkg@master + uses: julia-actions/julia-buildpkg@v1 - name: Running - uses: julia-actions/julia-runtest@master + uses: julia-actions/julia-runtest@v1 - name: Process coverage uses: julia-actions/julia-processcoverage@v1 - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./lcov.info - name: Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index 729b3417..1e0c1330 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.jl.mem /Manifest.toml test/Manifest.toml +docs/Manifest.toml diff --git a/NEWS.md b/NEWS.md index 836e2a62..dd419b90 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # NEWS +- v0.4.24 + - Fix workflows - v0.4.23 - Fix type warnings - v0.4.22 diff --git a/Project.toml b/Project.toml index 0c95e791..15d8c77f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,16 @@ name = "Phylo" uuid = "aea672f4-3940-5932-aa44-993d1c3ff149" author = ["Richard Reeve "] -version = "0.4.23" +version = "0.4.24" [deps] AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -IterableTables = "1c8ee90f-4401-5389-894e-7a04a3dc0f4d" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +IterableTables = "1c8ee90f-4401-5389-894e-7a04a3dc0f4d" Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -20,17 +21,40 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" +[weakdeps] +RCall = "6f49c342-dc21-5d91-9882-a32aef131414" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" + +[extensions] +PhyloRCallExt = "RCall" + [compat] AxisArrays = "0.4" -DataFrames = "^1" +DataFrames = "1" DataStructures = "0.17, 0.18" +Distances = "0.10" Distributions = "0.24, 0.25" -IterableTables = "^1" -Graphs = "^1" -Missings = "^1" -RecipesBase = "^1" -Requires = "^1" +Graphs = "1" +IterableTables = "1" +Missings = "1" +Plots = "1" +Printf = "1" +RCall = "0.13" +Random = "1" +RecipesBase = "1" +Requires = "1" SimpleTraits = "0.9" +Statistics = "1" +Test = "1" Tokenize = "0.5" -Unitful = "^1" -julia = "^1" +Unitful = "1" +julia = "1" + +[extras] +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +RCall = "6f49c342-dc21-5d91-9882-a32aef131414" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["DataFrames", "Plots", "RCall", "Test"] diff --git a/docs/make.jl b/docs/make.jl index def9d8b1..fd3246d2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -3,25 +3,22 @@ using Phylo makedocs(modules = [Phylo, Phylo.API], sitename = "Phylo.jl", - pages = [ - "Home" => "index.md", - "Tutorial / Quick start" => "tutorial.md", - "Manual" => Any[ - "Phylogeny data types" => "man/treetypes.md", - "Creating phylogenies" => "man/input.md", - "Manipulating and building phylogenies" => "man/manipulating.md", - "Traversal and iterators" => "man/traversal.md", - "Getting phylogeny attributes" => "man/attributes.md", - "Modelling traits" => "man/modelling.md", - "Plotting" => "man/plotting.md" - ], - "List of functions" => "functionlist.md", - "API" => "api.md" - ]) + pages = ["Home" => "index.md", + "Tutorial / Quick start" => "tutorial.md", + "Manual" => Any[ + "Phylogeny data types" => "man/treetypes.md", + "Creating phylogenies" => "man/input.md", + "Manipulating and building phylogenies" => "man/manipulating.md", + "Traversal and iterators" => "man/traversal.md", + "Getting phylogeny attributes" => "man/attributes.md", + "Modelling traits" => "man/modelling.md", + "Plotting" => "man/plotting.md" + ], + "List of functions" => "functionlist.md", + "API" => "api.md"]; + format = Documenter.HTML(size_threshold_ignore = ["man/plotting.md", + "man/input.md"])) deploydocs(repo = "github.com/EcoJulia/Phylo.jl.git", devbranch = "dev", - deps = Deps.pip("pygments", - "mkdocs", - "mkdocs-material", - "python-markdown-math")) + push_preview = true) diff --git a/docs/src/api.md b/docs/src/api.md index 570ceb8d..e98e7e07 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -9,13 +9,13 @@ Providing additional code to extend the functionality of the system is simple: ```julia using Phylo -importall Phylo.API -type SimplestTree <: AbstractTree{Int, Int} +struct SimplestTree <: AbstractTree{Int, Int} nodes::OrderedDict{Int, BinaryNode{Int}} branches::Dict{Int, Branch{Int}} end +import Phylo.API: _addnode! function _addnode!(tree::SimplestTree, num) _setnode!(tree, num, BinaryNode{Int}()) return num @@ -31,7 +31,6 @@ implementations. ```@autodocs Modules = [Phylo.API] -Private = false ``` ```@index diff --git a/docs/src/functionlist.md b/docs/src/functionlist.md index 011bf3a1..60f3dc4c 100644 --- a/docs/src/functionlist.md +++ b/docs/src/functionlist.md @@ -1,3 +1,4 @@ +# Function list ```@index ``` diff --git a/docs/src/man/attributes.md b/docs/src/man/attributes.md index f2dff529..8c3e8267 100644 --- a/docs/src/man/attributes.md +++ b/docs/src/man/attributes.md @@ -1,15 +1,18 @@ # Getting tree attributes -### Methods on TreeSets +## Methods on TreeSets + ```@docs ntrees +gettree gettrees nroots getroots gettreenames ``` -### Methods on Trees +## Methods on Trees + ```@docs mrca nodeheights @@ -17,24 +20,53 @@ getleafnames getleaves nleaves nnodes +ninternal nbranches distance distances heighttoroot heightstoroot +getroot +treenametype +gettreename +roottype +nodetype +nodedatatype +nodenametype +branchtype +branchdatatype +branchnametype +getnodenames +getnodename +hasnode +getnode +getnodes +getinternalnodes +getbranchnames +getbranchname +hasbranch +getbranch +getbranches +gettreeinfo +validate! +branchdims +treetype ``` -### Methods on Nodes +## Methods on Nodes + ```@docs isleaf isroot isinternal isunattached +degree indegree outdegree hasinbound getinbound getoutbounds +getconnections hasoutboundspace hasinboundspace getleafinfo @@ -49,12 +81,17 @@ getparent getancestors getchildren getdescendants +getsiblings ``` -### Methods on Branches +## Methods on Branches + ```@docs src dst +conn +conns +haslength getlength hasrootheight getrootheight @@ -62,33 +99,3 @@ setrootheight! getbranchdata setbranchdata! ``` - - - -```@docs -getroot -treenametype -gettreename -roottype -nodetype -nodedatatype -nodenametype -branchtype -branchdatatype -branchnametype - -getnodenames -getnodename -hasnode -getnode -getnodes -getbranchnames -getbranchname -hasbranch -getbranch -getbranches -validate! -branchdims -``` - - diff --git a/docs/src/man/input.md b/docs/src/man/input.md index f6a86c96..baa0a74d 100644 --- a/docs/src/man/input.md +++ b/docs/src/man/input.md @@ -1,18 +1,22 @@ # Creating phylogenies -### Reading phylogenies from disk +## Reading phylogenies from disk + Phylo can read newick trees either from strings, + ```@example reading using Phylo simpletree = parsenewick("((,Tip:1.0)Internal,)Root;") ``` which will result in the following tree: + ```@example reading getbranches(simpletree) ``` or from files + ```@example reading tree = open(parsenewick, Phylo.path("H1N1.newick")) ``` @@ -25,6 +29,7 @@ ts = open(parsenexus, Phylo.path("H1N1.trees")) Reading multiple trees from a nexus file returns a `TreeSet` - index to get the individual trees + ```@example reading gettreeinfo(ts) ``` @@ -33,10 +38,12 @@ gettreeinfo(ts) ts["TREE1"] ``` -### Creating random phylogenies +## Creating random phylogenies + The package can be used to generate random trees using the framework from `Distributions`. For instance, to construct a sampler for 5 tip non-ultrametric trees and generate a random tree of that type + ```@example random_trees using Phylo nu = Nonultrametric(5); @@ -44,15 +51,17 @@ tree = rand(nu) ``` Or two trees + ```@example random_trees trees = rand(nu, ["Tree 1", "Tree 2"]) ``` -### Importing phylogenies from R +## Importing phylogenies from R + Phylo allows transferring trees from R's `ape` package directly via RCall. -This allows any existing R library functions to be carried out on julia trees. +This allows any existing R library functions to be carried out on julia trees. Naturally the medium-term plan is to make this package feature-complete -with existing functionality in R, and as a result the R interface is not built +with existing functionality in R, and as a result the R interface is not built into the package, avoiding having RCall (and R) a dependency. Instead, if you want to use the R interface you need to do it manually, as below: @@ -109,4 +118,5 @@ R> if (all.equal(rt, jt)) "no damage in translation" parsenewick parsenexus Nonultrametric -Ultrametric \ No newline at end of file +Ultrametric +``` diff --git a/docs/src/man/plotting.md b/docs/src/man/plotting.md index a3d9a9ef..5c5f09e9 100644 --- a/docs/src/man/plotting.md +++ b/docs/src/man/plotting.md @@ -3,22 +3,25 @@ Phylo defines recipes for all `AbstractTree`s, allowing them to be plotted with [Plots.jl](https://docs.juliaplots.org/latest). -### Keywords +## Keywords It adds these keywords to the ones initially supported by Plots.jl: + - treetype: choosing `:fan` or `:dendrogram` determines the shape of the tree - marker_group: applies the `group` keyword to node markers - line_group: applies the `group` keyword to branch lines - showtips: `true` (the default) shows the leaf names - tipfont: a tuple defining the font to use for leaf names (default is `(7,)`), -which sets the font size to 7 - for font definitions see +which sets the font size to 7 - for font definitions see [Plots annotation fonts](https://docs.juliaplots.org/latest/generated/gr/#gr-ref20) -### Example plots +## Example plots + For this example, we will use the phylogeny of all extant hummingbird species. -Read the tree and plot it using two different `treetype`s. The +Read the tree and plot it using two different `treetype`s. The default is `:dendrogram` + ```@example plotting using Phylo, Plots ENV["GKSwstype"]="nul" # hide @@ -28,42 +31,49 @@ plot(hummers, size = (400, 600), showtips = false) ``` For larger trees, the `:fan` treetype may work better + ```@example plotting plot(hummers, treetype = :fan) ``` -### Sorting trees for plotting +## Sorting trees for plotting + Many phylogenies look more aesthetically pleasing if the descendants from each node are sorted in order of their size. This is called `ladderize` in some other packages. + ```@example plotting sort!(hummers, rev = true) plot(hummers, treetype = :fan) ``` -### Coloring branches or nodes by a variable +## Coloring branches or nodes by a variable + It is common in evolutionary studies to color the branches or node markers with -the value of some variable. Plots already offers the keyword attributes +the value of some variable. Plots already offers the keyword attributes `marker_z` and `line_z` for these uses, and they also work on Phylo objects. -We can pass either -- a `Vector` with the same number of elements as there are +We can pass either + +- a `Vector` with the same number of elements as there are branches / internal nodes, where the values follow a depthfirst order (because the tree is plotted in depthfirst order); -- a `Dict` of `node => value`, with the value to be plotted for each node +- a `Dict` of `node => value`, with the value to be plotted for each node (skipping nodes not in the Dict). To demonstrate, let's start by defining a custom function for evolving a trait -on the phylogeny according to Brownian motion, using the utility function +on the phylogeny according to Brownian motion, using the utility function `map_depthfirst` + ```@example plotting evolve(tree) = map_depthfirst((val, node) -> val + randn(), 0., tree, Float64) trait = evolve(hummers) plot(hummers, treetype = :fan, line_z = trait, linecolor = :RdYlBu, linewidth = 5, showtips = false) ``` -The inbuilt facilities for sampling traits on trees on Phylo returns a +The inbuilt facilities for sampling traits on trees on Phylo returns a `Node => value` Dict, which can also be passed to `marker_z` + ```@example plotting brownsampler = BrownianTrait(hummers, "Trait") plot(hummers, @@ -71,10 +81,10 @@ plot(hummers, linewidth = 2, markercolor = :RdYlBu, size = (400, 600)) ``` - We can also use the `map_depthfirst` utility function to highlight the clade -descending from, e.g., Node 248. Here, the recursive function creates a vector +descending from, e.g., Node 248. Here, the recursive function creates a vector of colors which will all be orange after encountering Node 248 but black before + ```@example plotting clade_color = map_depthfirst((val, node) -> node == "Node 248" ? :orange : val, :black, hummers) plot(hummers, linecolor = clade_color, showtips = false, linewidth = 2, size = (400, 600)) @@ -84,6 +94,7 @@ The Plots attributes related to markers (`markersize`, `markershape` etc.) will put markers on the internal nodes (or on both internal and tip nodes if a longer) vector is passed). The `series_attributes` keyword is also supported and behaves the same way + ```@example plotting plot(hummers, size = (400, 800), @@ -99,21 +110,23 @@ nodes or branches within the phylogeny. Let's randomly evolve a discrete trait for temperature preference on the tree, using `rand!` to add the modelled values to the tree's list of node data. -In addition to taking a Vector or a Dict, `marker_group` also accepts the name +In addition to taking a Vector or a Dict, `marker_group` also accepts the name of internal node data. + ``` @example plotting -# evolve the trait and add it to the tree +## evolve the trait and add it to the tree using Random @enum TemperatureTrait lowTempPref midTempPref highTempPref tempsampler = SymmetricDiscreteTrait(hummers, TemperatureTrait, 0.4, "Temperature") rand!(tempsampler, hummers) -# and plot it +## and plot it plot(hummers, showtips = false, marker_group = "Temperature", legend = :topleft, msc = :white, treetype = :fan, c = [:red :blue :green]) ``` + ```@docs map_depthfirst sort diff --git a/docs/src/man/treetypes.md b/docs/src/man/treetypes.md index f06238b7..a65742f5 100644 --- a/docs/src/man/treetypes.md +++ b/docs/src/man/treetypes.md @@ -1,30 +1,51 @@ -# Data types +# Modules and data types -### Tree sets +## Modules -### Tree types -This package offers a number of different types of tree, each -optimised for a specific usage +```@autodocs +Modules = [Phylo] +Order = [:module] +``` -### Node types +## Tree sets +```@docs +TreeSet +``` -### Branch types +## Tree types +This package offers a number of different types of tree, each +optimised for a specific usage ```@docs -BinaryTree +LinkTree NamedBinaryTree -BinaryNode -Node +BinaryTree NamedTree PolytomousTree NamedPolytomousTree -LinkTree -LinkBranch +``` + +## Node types + +```@docs LinkNode -RootedTree -ManyRootTree -UnrootedTree -TreeSet -``` \ No newline at end of file +BinaryNode +Node +``` + +## Branch types + +```@docs +LinkBranch +Branch +``` + +## Iterator types + +```@autodocs +Modules = [Phylo, Phylo.API] +Public = false +Order = [:type] +``` diff --git a/src/rcall.jl b/ext/PhyloRCallExt.jl similarity index 84% rename from src/rcall.jl rename to ext/PhyloRCallExt.jl index cb43527f..cf7c3851 100644 --- a/src/rcall.jl +++ b/ext/PhyloRCallExt.jl @@ -1,7 +1,20 @@ +module PhyloRCallExt + +if isdefined(Base, :get_extension) + using RCall + using RCall: protect, unprotect, rcall_p, RClass, isObject, isS4 + import RCall: rcopy + import RCall: rcopytype + import RCall: sexp +else + using ..RCall + using ..RCall: protect, unprotect, rcall_p, RClass, isObject, isS4 + import ..RCall: rcopy + import ..RCall: rcopytype + import ..RCall: sexp +end + using Phylo -using .RCall -using .RCall: protect, unprotect, rcall_p, RClass, isObject, isS4 -import .RCall: rcopy function rcopy(::Type{T}, rt::Ptr{VecSxp}) where T <: AbstractTree if !isObject(rt) || isS4(rt) || rcopy(String, rcall_p(:class, rt)) != "phylo" @@ -31,12 +44,9 @@ function rcopy(::Type{T}, rt::Ptr{VecSxp}) where T <: AbstractTree return tree end -import .RCall.rcopytype rcopytype(::Type{RClass{:phylo}}, s::Ptr{VecSxp}) = RootedTree -import .RCall.sexp - function sexp(tree::T) where T <: AbstractTree validate!(tree) || @warn "Tree does not internally validate" leafnames = getleafnames(tree) @@ -70,3 +80,5 @@ function sexp(tree::T) where T <: AbstractTree unprotect(1) return sobj end + +end diff --git a/src/API.jl b/src/API.jl index 9a3354cb..fd80d99f 100644 --- a/src/API.jl +++ b/src/API.jl @@ -23,7 +23,7 @@ using Unitful @traitimpl MatchBranchType{T, B} <- _matchbranchtype(T, B) @traitdef MatchBranchNodeType{T, B, N} -@traitimpl MatchBranchNodeType{T, B, N} <- _matchbranchnodetype(T, N) +@traitimpl MatchBranchNodeType{T, B, N} <- _matchbranchnodetype(T, B, N) @traitdef MatchTreeNameType{T, TN} @traitimpl MatchTreeNameType{T, TN} <- _matchtreenametype(T, TN) @@ -62,7 +62,7 @@ _matchnodetype(::Type{<:AbstractTree{TT, RT, NL, N, B}}, _matchnodetype(::Type{<:AbstractTree{TT, RT, NL, N, B}}, ::Type{NL}) where {TT, RT, NL, N, B} = !_prefernodeobjects(N) _matchnodetypes(::Type{<:AbstractTree}, ::Type{<:Any}, ::Type{<:Any}) = false -_matchnodetypes(::Type{T}, ::Type{N}, ::Type{N}) where {T, N} = +_matchnodetypes(::Type{T}, ::Type{N}, ::Type{N}) where {T <: AbstractTree, N} = _matchnodetype(T, N) """ @@ -86,9 +86,8 @@ _matchbranchtype(::Type{<:AbstractTree{TT, RT, NL, N, B}}, Does this tree type prefer the branch and node types provided? """ function _matchbranchnodetype end -_matchbranchtype(::Type{T}, ::Type{B}, ::Type{N}) where - {TT, RT, NL, N, B, T <: AbstractTree{TT, RT, NL, N, B}} = - _matchbranchtype(T, B) && _matchnodetype(T, N) +_matchbranchnodetype(::Type{T}, ::Type{B}, ::Type{N}) where {T <: AbstractTree, B, N} = + _matchbranchtype(T, B) && _matchnodetype(T, N) """ _matchtreenametype(::Type{<:AbstractTree}, ::Type{X}) @@ -375,25 +374,32 @@ a branch or a pair) from a tree. Must be implemented for any PreferBranchObjects tree and branch label type. """ function _getbranch end -@traitfn _getbranch(tree::T, +@traitfn _getbranch(::T, branch::B) where {RT, NL, N, B, T <: AbstractTree{OneTree, RT, NL, N, B}; PreferBranchObjects{T}} = branch -@traitfn _getbranch(tree::T, +@traitfn _getbranch(::T, pair::Pair{Int, B}) where {RT, NL, N, B, T <: AbstractTree{OneTree, RT, NL, N, B}; PreferBranchObjects{T}} = pair[2] -@traitfn _getbranch(tree::T, +@traitfn _getbranch(::T, branchname::Int) where {T <: AbstractTree{OneTree}; !PreferBranchObjects{T}} = branchname -@traitfn _getbranch(tree::T, +@traitfn _getbranch(::T, pair::Pair{Int, B}) where {RT, NL, N, B, T <: AbstractTree{OneTree, RT, NL, N, B}; !PreferBranchObjects{T}} = pair[1] +@traitfn _getbranch(tree::T, src::N1, dst::N2) where + {T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} = + first(b for b in _getbranches(tree) if _src(tree, b) == src && _dst(tree, b) == dst) + +@traitfn _getbranch(tree::T, src::N1, dst::N2) where + {T <: AbstractTree{OneTree}, N1, N2; !MatchNodeTypes{T, N1, N2}} = + _getbranch(tree, _getnode(tree, src), _getnode(tree, dst)) """ _getbranchname(::AbstractTree, id) @@ -454,6 +460,11 @@ _hasbranch(tree::AbstractTree{OneTree}, PreferBranchObjects{T}} = branch ∈ _getbranches(tree) +@traitfn function _hasbranch(tree::T, src::N1, dst::N2) where + {T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} + return any(_src(tree, b) == src && _dst(tree, b) == dst for b in _getbranches(tree)) +end + """ _createbranch!(tree::AbstractTree, source, destination[, length][, data]) @@ -753,10 +764,10 @@ AbstractNode subtype, can be inferred from _getinbound and _getoutbounds for a rooted node. """ function _getconnections end -_getconnections(tree::AbstractTree{OneTree}, node) = +_getconnections(tree::AbstractTree{OneTree, <: Rooted}, node) = _hasinbound(tree, node) ? - append!([_getinbound(tree, node)], _getoutbounds(tree, node)) : - _getoutbounds(tree, node) + append!([_getinbound(tree, node)], _getoutbounds(tree, node)) : + _getoutbounds(tree, node) """ _getsiblings(tree::AbstractTree, node::AbstractNode) @@ -874,6 +885,15 @@ subtype. function _getlength end _getlength(::AbstractTree, _) = missing +""" + _haslength + +Return length of a branch. May be implemented for any AbstractBranch +subtype. +""" +function _haslength end +_haslength(t::AbstractTree, b) = !ismissing(_getlength(t, b)) + """ _leafinfotype(::Type{<:AbstractTree}) diff --git a/src/Interface.jl b/src/Interface.jl index 7d7a675e..0f2db891 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -1,5 +1,4 @@ using Phylo.API -import Graphs: src, dst, indegree, outdegree, degree using SimpleTraits # AbstractTree/Node/Branch type methods @@ -267,8 +266,7 @@ ManyTrees tree), or a Dict of vectors of node names for multiple trees. function getnodenames end getnodenames(tree::AbstractTree{OneTree}, order::TraversalOrder = preorder) = _getnodenames(tree, order) -getnodenames(trees::AbstractTree{ManyTrees}, name, - order::TraversalOrder = preorder) = +getnodenames(trees::AbstractTree{ManyTrees}, name, order::TraversalOrder = preorder) = getnodenames(gettree(trees, name), order) getnodenames(trees::AbstractTree{ManyTrees}, order::TraversalOrder = preorder) = Dict(name => getnodenames(gettree(trees, name), order) @@ -314,9 +312,9 @@ function createbranch! end name = missing, data = missing) where {T <: AbstractTree{OneTree, <: Rooted}, N1, N2; !MatchNodeTypes{T, N1, N2}} - hasnode(tree, src) || + _hasnode(tree, src) || error("Tree does not have an available source node - $src") - hasnode(tree, dst) || + _hasnode(tree, dst) || error("Tree does not have a destination node - $dst") sn = getnode(tree, src) dn = getnode(tree, dst) @@ -357,9 +355,9 @@ end name = missing, data = missing) where {T <: AbstractTree{OneTree, Unrooted}, N1, N2; !MatchNodeTypes{T, N1, N2}} - hasnode(tree, src) || + _hasnode(tree, src) || error("Tree does not have an available source node - $src") - hasnode(tree, dst) || + _hasnode(tree, dst) || error("Tree does not have a destination node - $dst") sn = getnode(tree, src) dn = getnode(tree, dst) @@ -424,7 +422,7 @@ end hasbranch(tree, src, dst) || error("Tree does not have a branch between " * "$(_getnodename(tree, src)) and $(_getnodename(tree, dst))") - return _deletebranch!(tree, getbranch(tree, src, dst)) + return _deletebranch!(tree, getbranch(tree, getnode(tree, src), getnode(tree, dst))) end @traitfn function deletebranch!(tree::T, src::N1, dst::N2) where {T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} @@ -476,8 +474,8 @@ end Delete a node (or a name) from a tree """ function deletenode! end -@traitfn deletenode!(tree::T, node::NL) where -{NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = +@traitfn deletenode!(tree::T, node::N) where +{T <: AbstractTree{OneTree}, N; !MatchNodeType{T, N}} = deletenode!(tree, getnode(tree, node)) @traitfn function deletenode!(tree::T, node::N) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -498,7 +496,7 @@ hasnode(tree::AbstractTree{OneTree}, node) = _hasnode(tree, node) Returns a node from a tree. """ function getnode(tree::AbstractTree{OneTree}, node) - hasnode(tree, node) || error("Node $node does not exist") + _hasnode(tree, node) || error("Node $node does not exist") return _getnode(tree, node) end @@ -510,10 +508,10 @@ node types, it will be able to extract the node name without reference to the tree. """ function getnodename end -@traitfn function getnodename(tree::T, node::NL) where - {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} - hasnode(tree, node) || error("Node $node does not exist") - return _getnodename(tree, node) +@traitfn function getnodename(tree::T, node::N) where + {T <: AbstractTree{OneTree}, N; !MatchNodeType{T, N}} + _hasnode(tree, node) || error("Node $node does not exist") + return _getnodename(tree, getnode(tree, node)) end @traitfn function getnodename(tree::T, node::N) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -529,17 +527,17 @@ Does `tree` have a branch `branch` or a branch from `source` to `dest`? """ function hasbranch end @traitfn hasbranch(tree::T, branch::B) where -{T <: AbstractTree{OneTree}, B; MatchBranchType{T, B}} = + {T <: AbstractTree{OneTree}, B; MatchBranchType{T, B}} = _hasbranch(tree, branch) @traitfn hasbranch(tree::T, branch::B) where -{T <: AbstractTree{OneTree}, B; !MatchBranchType{T, B}} = + {T <: AbstractTree{OneTree}, B; !MatchBranchType{T, B}} = _hasbranch(tree, _getbranch(tree, branch)) @traitfn hasbranch(tree::T, src::N1, dst::N2) where -{T <: AbstractTree{OneTree}, N1, N2; !MatchNodeTypes{T, N1, N2}} = + {T <: AbstractTree{OneTree}, N1, N2; !MatchNodeTypes{T, N1, N2}} = hasnode(tree, src) && hasnode(tree, dst) && _hasbranch(tree, _getnode(tree, src), _getnode(tree, dst)) @traitfn hasbranch(tree::T, src::N1, dst::N2) where -{T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} = + {T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} = _hasnode(tree, src) && _hasnode(tree, dst) && _hasbranch(tree, src, dst) """ @@ -560,10 +558,10 @@ end return branch end @traitfn getbranch(tree::T, src::N1, dst::N2) where -{T <: AbstractTree{OneTree}, N1, N2; !MatchNodeTypes{T, N1, N2}} = + {T <: AbstractTree{OneTree}, N1, N2; !MatchNodeTypes{T, N1, N2}} = getbranch(tree, getnode(tree, src), getnode(tree, dst)) @traitfn getbranch(tree::T, src::N1, dst::N2) where -{T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} = + {T <: AbstractTree{OneTree}, N1, N2; MatchNodeTypes{T, N1, N2}} = hasnode(tree, src) && hasnode(tree, dst) && _getbranch(tree, src, dst) """ @@ -865,7 +863,7 @@ function getchildren end @traitfn function getchildren(tree::T, node::NL) where {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} hasnode(tree, node) || error("Node $node does not exist") - return _getnodename.(tree, _getchildren(tree, _getnode(tree, node))) + return _getnodename.(Ref(tree), _getchildren(tree, _getnode(tree, node))) end @traitfn function getchildren(tree::T, node::N) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} @@ -899,7 +897,7 @@ a rooted node. """ function getsiblings end @traitfn getsiblings(tree::T, node::NL) where -{NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = + {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = _getsiblings(tree, getnode(tree, node)) @traitfn function getsiblings(tree::T, node::N) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -914,12 +912,11 @@ Does the node have a height defined? """ function hasheight end @traitfn hasheight(tree::T, node::NL) where -{NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = + {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = hasheight(tree, getnode(tree, node)) -@traitfn function hasheight(tree::AbstractTree{OneTree, <: Rooted}, - node::N) where +@traitfn function hasheight(tree::T, node::N) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} - hasnode(tree, node) || error("Node $node does not exist") + _hasnode(tree, node) || error("Node $node does not exist") return _hasheight(tree, node) || (_hasrootheight(tree) && mapreduce(b -> haslength(tree, b), &, branchhistory(tree, node); @@ -933,7 +930,7 @@ Return the height of the node. """ function getheight end @traitfn getheight(tree::T, node::NL) where -{NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = + {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = getheight(tree, getnode(tree, node)) @traitfn function getheight(tree::T, node::N) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} @@ -952,7 +949,7 @@ Set the height of the node. """ function setheight! end @traitfn setheight!(tree::T, node::NL, height::Number) where -{NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = + {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} = _setheight!(tree, getnode(tree, node), height) @traitfn function setheight!(tree::T, node::N, height::Number) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} @@ -968,7 +965,7 @@ Return the source node for this branch. """ function src end @traitfn src(tree::T, branch::B) where -{T <: AbstractTree{OneTree, <: Rooted}, B; !MatchBranchType{T, B}} = + {T <: AbstractTree{OneTree, <: Rooted}, B; !MatchBranchType{T, B}} = _src(tree, getbranch(tree, branch)) @traitfn function src(tree::T, branch::B) where {T <: AbstractTree{OneTree, <: Rooted}, B; MatchBranchType{T, B}} @@ -1018,8 +1015,8 @@ Return the other node connected to `branch` that is not `exclude`. function conn end @traitfn function conn(tree::T, branch::B, exclude::N) where {T <: AbstractTree{OneTree}, B, N; !MatchBranchNodeType{T, B, N}} - hasbranch(tree, branch) || error("Branch $branch does not exist") - hasnode(tree, exclude) || error("Node $exclude does not exist") + _hasbranch(tree, branch) || error("Branch $branch does not exist") + _hasnode(tree, exclude) || error("Node $exclude does not exist") return _conn(tree, _getbranch(tree, branch), _getnode(tree, exclude)) end @traitfn function conn(tree::T, branch::B, exclude::N) where @@ -1046,6 +1043,23 @@ end return _getlength(tree, branch) end +""" + haslength(tree::AbstractTree, branch) + +Return whether the branch has a length. +""" +function haslength end +@traitfn function haslength(tree::T, branch::B) where + {T <: AbstractTree{OneTree}, B; !MatchBranchType{T, B}} + hasbranch(tree, branch) || error("Branch $branch does not exist") + return _haslength(tree, _getbranch(tree, branch)) +end +@traitfn function haslength(tree::T, branch::B) where + {T <: AbstractTree{OneTree}, B; MatchBranchType{T, B}} + _hasbranch(tree, branch) || error("Branch $branch does not exist") + return _haslength(tree, branch) +end + """ getleafnames(::AbstractTree[, ::TraversalOrder]) @@ -1076,7 +1090,7 @@ retrieve the leaf info for a leaf of the tree. function getleafinfo end getleafinfo(tree::AbstractTree) = _getleafinfo(tree) @traitfn getleafinfo(tree::T, leaf::NL) where -{RT, NL, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = + {RT, NL, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = _getleafinfo(tree, getnode(tree, leaf)) @traitfn function getleafinfo(tree::T, leaf::N) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -1101,7 +1115,7 @@ retrieve the node data for a node of the tree. """ function getnodedata end @traitfn getnodedata(tree::T, node::NL) where -{NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = + {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = _getnodedata(tree, getnode(tree, node)) @traitfn function getnodedata(tree::T, node::N) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -1125,7 +1139,7 @@ Set the node data for a node of the tree. """ function setnodedata! end @traitfn setnodedata!(tree::T, node::NL, label, value) where -{NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = + {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = _setnodedata!(tree, getnode(tree, node), label, value) @traitfn function setnodedata!(tree::T, node::N, label, value) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -1133,7 +1147,7 @@ function setnodedata! end return _setnodedata!(tree, node, label, value) end @traitfn setnodedata!(tree::T, node::NL, data) where -{NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = + {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} = _setnodedata!(tree, getnode(tree, node), data) @traitfn function setnodedata!(tree::T, node::N, data) where {T <: AbstractTree{OneTree}, N; MatchNodeType{T, N}} @@ -1197,7 +1211,6 @@ function validate!(tree::T) where nodes = _getnodes(tree) nodenames = _getnodenames(tree) branches = _getbranches(tree) - branchnames = _getbranchnames(tree) if !isempty(nodes) || !isempty(branches) # We need to validate the connections if Set(_getinbound(tree, node) for node in nodes @@ -1240,7 +1253,7 @@ traversal(tree::AbstractTree{OneTree}, order::TraversalOrder = preorder) = _traversal(tree, order) @traitfn function traversal(tree::T, order::TraversalOrder, init::NL) where {NL, RT, T <: AbstractTree{OneTree, RT, NL}; !MatchNodeType{T, NL}} - hasnode(tree, init) || error("Node $init does not exist") + _hasnode(tree, init) || error("Node $init does not exist") return _getnodename.(tree, _traversal(tree, order, [getnode(tree, init)])) end @traitfn function traversal(tree::T, order::TraversalOrder, init::N) where @@ -1257,12 +1270,12 @@ Return the name of all of the nodes that are ancestral to this node. function getancestors end @traitfn function getancestors(tree::T, init::NL) where {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} - hasnode(tree, init) || error("Node $init does not exist") - return getnodename.(tree, _treehistory(tree, getnode(tree, init))[2][2:end]) + _hasnode(tree, init) || error("Node $init does not exist") + return getnodename.(Ref(tree), _treehistory(tree, getnode(tree, init))[2][2:end]) end @traitfn function getancestors(tree::T, init::N) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} - hasnode(tree, init) || error("Node $init does not exist") + _hasnode(tree, init) || error("Node $init does not exist") return _treehistory(tree, init)[2][2:end] end @@ -1274,11 +1287,11 @@ Return the names of all of the nodes that descend from this node. function getdescendants end @traitfn function getdescendants(tree::T, init::NL) where {NL, T <: AbstractTree{OneTree, <: Rooted, NL}; !MatchNodeType{T, NL}} - hasnode(tree, init) || error("Node $init does not exist") - return getnodename.(tree, _treefuture(tree, getnode(tree, init))[2][2:end]) + _hasnode(tree, init) || error("Node $init does not exist") + return getnodename.(Ref(tree), _treefuture(tree, getnode(tree, init))[2][2:end]) end @traitfn function getdescendants(tree::T, init::N) where {T <: AbstractTree{OneTree, <: Rooted}, N; MatchNodeType{T, N}} - hasnode(tree, init) || error("Node $init does not exist") + _hasnode(tree, init) || error("Node $init does not exist") return _treefuture(tree, init)[2][2:end] end diff --git a/src/Iterators.jl b/src/Iterators.jl index 90143a3c..af29c926 100644 --- a/src/Iterators.jl +++ b/src/Iterators.jl @@ -39,6 +39,11 @@ function length(bi::It) where It <: AbstractBranchIterator count(val -> bi.filterfn(bi.tree, _getbranch(bi.tree, val)), bi) end +""" + NodeIterator + +The struct representing an iterator for nodes of a phylogenetic tree +""" struct NodeIterator{T <: AbstractTree} <: AbstractNodeIterator{T} tree::T filterfn::Union{Function, Nothing} @@ -61,6 +66,11 @@ nodefilter(filterfn::Function, tree::T) where T <: AbstractTree = eltype(ni::NodeIterator{T}) where T <: AbstractTree = nodetype(T) +""" + NodeNameIterator + +The struct representing an iterator for nodenames of a phylogenetic tree +""" struct NodeNameIterator{T <: AbstractTree} <: AbstractNodeIterator{T} tree::T filterfn::Union{Function, Nothing} @@ -85,6 +95,11 @@ nodenamefilter(filterfn::Function, tree::T) where T <: AbstractTree = eltype(ni::NodeNameIterator{T}) where T <: AbstractTree = nodenametype(T) +""" + BranchIterator + +The struct representing an iterator for branches of a phylogenetic tree +""" struct BranchIterator{T <: AbstractTree} <: AbstractBranchIterator{T} tree::T filterfn::Union{Function, Nothing} @@ -109,6 +124,11 @@ branchfilter(filterfn::Function, tree::T) where T <: AbstractTree = eltype(bi::BranchIterator{T}) where T <: AbstractTree = branchtype(T) +""" + BranchNameIterator + +The struct representing an iterator for branchnames of a phylogenetic tree +""" struct BranchNameIterator{T <: AbstractTree} <: AbstractBranchIterator{T} tree::T filterfn::Union{Function, Nothing} diff --git a/src/LinkTree.jl b/src/LinkTree.jl index f505e463..676089e3 100644 --- a/src/LinkTree.jl +++ b/src/LinkTree.jl @@ -4,6 +4,11 @@ using Unitful newempty(::Type{Data}) where Data = Data() +""" + struct LinkBranch <: AbstractBranch + +A branch type that connects LinkNodes in a LinkTree +""" struct LinkBranch{RT, NL, Data, LenUnits} <: AbstractBranch{RT, NL} name::Int inout::Tuple{AbstractNode{RT, NL}, AbstractNode{RT, NL}} @@ -11,6 +16,11 @@ struct LinkBranch{RT, NL, Data, LenUnits} <: AbstractBranch{RT, NL} data::Data end +""" + struct LinkNode <: AbstractNode + +A node type that is connected by LinkBranches in a LinkTree +""" mutable struct LinkNode{RT, NL, Data, B <: AbstractBranch{RT, NL}} <: AbstractNode{RT, NL} name::NL @@ -25,6 +35,11 @@ end import Phylo.API: _prefernodeobjects _prefernodeobjects(::Type{<:LinkNode}) = true +""" + struct LinkTree <: AbstractTree + +A phylogenetic tree type containing LinkNodes and LinkBranches +""" mutable struct LinkTree{RT, NL, N <: LinkNode{RT, NL}, B <: LinkBranch{RT, NL}, TD} <: AbstractTree{OneTree, RT, NL, N, B} @@ -58,6 +73,7 @@ mutable struct LinkTree{RT, NL, N <: LinkNode{RT, NL}, return tree end end + function LinkTree{RT, NL, N, B, TD}(leafinfos::TD) where {RT, NL, N, B, TD} leafnames = unique(info[1] for info in getiterator(leafinfos)) return LinkTree{RT, NL, N, B, TD}(leafnames; tipdata = leafinfos) @@ -98,8 +114,7 @@ _src(::AbstractTree, branch::LinkBranch{<:Rooted}) = branch.inout[1] import Phylo.API: _dst _dst(::AbstractTree, branch::LinkBranch{<:Rooted}) = branch.inout[2] import Phylo.API: _conn -function _conn(::AbstractTree, branch::LinkBranch{RT, NL, D}, - exclude::AbstractNode{RT, NL}) where {RT, NL, D} +function _conn(::LinkTree, branch::LinkBranch, exclude::LinkNode) return exclude ≡ branch.inout[1] ? branch.inout[2] : (exclude ≡ branch.inout[2] ? branch.inout[1] : error("Branch $(branch.name) not connected to $(exclude.name)")) @@ -287,7 +302,7 @@ import Phylo.API: _getnode node::NL) where {RT, NL, T <: LinkTree{RT, NL}; PreferNodeObjects{T}} = - tree.nodes[tree.nodedict[node]] + tree.nodes[tree.nodedict[node]] import Phylo.API: _getbranches _getbranches(tree::LinkTree) = skipmissing(tree.branches) @@ -436,15 +451,15 @@ import Base.show function show(io::IO, node::LinkNode{Unrooted}) print(io, "Unrooted LinkNode '$(node.name)', with $(length(node.other)) connection(s).") if length(node.other) == 0 - println(io, "a node with no connections.") + print(io, "a node with no connections.") elseif length(node.other) == 1 - println(io, "a node with 1 connection (branch $(node.other[1].name))") + print(io, "a node with 1 connection (branch $(node.other[1].name))") else print(io, "a node with $(length(node.other)) outbound connections (branches $(node.other[1].name)") for i in 2:length(node.other) print(io, ", $(node.other[i].name)") end - println(io, ")") + print(io, ")") end end @@ -452,35 +467,35 @@ function show(io::IO, node::LinkNode) print(io, "LinkNode $(node.name), ") if ismissing(node.inbound) if length(node.other) == 0 - println(io, "an isolated node with no connections.") + print(io, "an isolated node with no connections.") elseif length(node.other) == 1 - println(io, "a root node with 1 outbound connection (branch $(node.other[1].name))") + print(io, "a root node with 1 outbound connection (branch $(node.other[1].name))") else print(io, "a root node with $(length(node.other)) outbound connections (branches $(node.other[1].name)") for i in 2:length(node.other) print(io, ", $(node.other[i].name)") end - println(io, ")") + print(io, ")") end else if length(node.other) == 0 - println(io, "a tip of the tree with an incoming connection (branch $(node.inbound.name)).") + print(io, "a tip of the tree with an incoming connection (branch $(node.inbound.name)).") elseif length(node.other) == 1 - println(io, "an internal node with 1 inbound and 1 outbound connection (branches $(node.inbound.name) and $(node.other[1].name))") + print(io, "an internal node with 1 inbound and 1 outbound connection (branches $(node.inbound.name) and $(node.other[1].name))") else print(io, "an internal node with 1 inbound and $(length(node.other)) outbound connections (branches $(node.inbound.name) and $(node.other[1].name)") for i in 2:length(node.other) print(io, ", $(node.other[i].name)") end - println(io, ")") + print(io, ")") end end end function show(io::IO, branch::LinkBranch{Unrooted}) - println(io, "Unrooted LinkBranch $(branch.name), connecting nodes $(branch.inout[1].name) and $(branch.inout[2].name)$(ismissing(branch.length) ? "" : " (length $(branch.length))").") + print(io, "Unrooted LinkBranch $(branch.name), connecting nodes $(branch.inout[1].name) and $(branch.inout[2].name)$(ismissing(branch.length) ? "" : " (length $(branch.length))").") end function show(io::IO, branch::LinkBranch) - println(io, "LinkBranch $(branch.name), from node $(branch.inout[1].name) to node $(branch.inout[2].name)$(ismissing(branch.length) ? "" : " (length $(branch.length))").") + print(io, "LinkBranch $(branch.name), from node $(branch.inout[1].name) to node $(branch.inout[2].name)$(ismissing(branch.length) ? "" : " (length $(branch.length))").") end diff --git a/src/Node.jl b/src/Node.jl index 74b40e23..7833e271 100644 --- a/src/Node.jl +++ b/src/Node.jl @@ -1,5 +1,5 @@ """ - BinaryNode{B}(AbstractVector{B}, AbstractVector{B}) <: AbstractNode + struct BinaryNode <: AbstractNode A node of strict binary phylogenetic tree """ @@ -29,7 +29,7 @@ import Phylo.API: _prefernodeobjects _prefernodeobjects(::Type{<:BinaryNode}) = false """ - Node{RT, NL, T}(AbstractVector{T}, AbstractVector{T}) <: AbstractNode + struct Node <: AbstractNode A node of potentially polytomous phylogenetic tree """ @@ -52,49 +52,3 @@ mutable struct Node{RT <: Rooted, NL, B <: AbstractBranch{RT, NL}} <: end _prefernodeobjects(::Type{<:Node}) = false - -import Phylo.API: _getinbound -function _getinbound(tree::AbstractTree, node::Node) - _hasinbound(tree, node) || - error("Node has no inbound connection") - return node.inbound -end - -import Phylo.API: _addinbound! -function _addinbound!(tree::AbstractTree, node::Node{RT, NL, B}, - inbound::B) where {RT, NL, B <: AbstractBranch} - _hasinbound(tree, node) && - error("Node already has an inbound connection") - node.inbound = inbound -end - -import Phylo.API: _removeinbound! -function _removeinbound!(tree::AbstractTree, - node::Node{RT, NL, B}, - inbound::B) where {RT, NL, B <: AbstractBranch} - _hasinbound(tree, node) || error("Node has no inbound connection") - node.inbound == inbound || - error("Node has no inbound connection from branch $inbound") - node.inbound = nothing -end - -import Phylo.API: _getoutbounds -function _getoutbounds(::AbstractTree, node::Node) - return node.outbounds -end - -import Phylo.API: _addoutbound! -function _addoutbound!(::AbstractTree, node::Node{RT, NL, B}, - outbound::B) where {RT, NL, B <: AbstractBranch} - push!(node.outbounds, outbound) -end - -import Phylo.API: _removeoutbound! -function _removeoutbound!(::AbstractTree, node::Node{RT, NL, B}, - outbound::B) where {RT, NL, B <: AbstractBranch} - outbound ∈ node.outbounds ? filter!(n -> n != outbound, node.outbounds) : - error("Node does not have outbound connection to branch $outbound") -end - -import Phylo.API: _getnodename -_getnodename(::AbstractTree, node::Node) = node.name diff --git a/src/Phylo.jl b/src/Phylo.jl index 8b5f21d1..7432c9df 100644 --- a/src/Phylo.jl +++ b/src/Phylo.jl @@ -19,6 +19,7 @@ interact cleanly with other phylogenetic packages. module Phylo import Base: Pair, Tuple, show, eltype, length, getindex +import Graphs: src, dst, indegree, outdegree, degree abstract type Rootedness end struct Unrooted <: Rootedness end abstract type Rooted <: Rootedness end @@ -34,9 +35,12 @@ export OneTree, ManyTrees abstract type AbstractNode{RootType <: Rootedness, NodeLabel} end abstract type AbstractBranch{RootType <: Rootedness, NodeLabel} end +using Distances abstract type AbstractTree{TT <: TreeType, RT <: Rootedness, NL, N <: AbstractNode{RT, NL}, - B <: AbstractBranch{RT, NL}} end + B <: AbstractBranch{RT, NL}} <: Distances.UnionMetric +end + export AbstractTree @enum TraversalOrder anyorder preorder inorder postorder breadthfirst @@ -55,7 +59,7 @@ include("API.jl") export _ntrees, _gettrees, _nroots, _getroots, _getroot export _treenametype, _gettreenames, _gettree, _gettreename export _createbranch!, _deletebranch!, _createnode!, _deletenode! -export _getnodenames, _hasnode, _getnode, _getnodes +export _getnodenames, _getnodename, _hasnode, _getnode, _getnodes export _getbranchnames, _getbranchname, _hasbranch, _getbranch, _getbranches export _hasrootheight, _getrootheight, _setrootheight!, _clearrootheight! export _getleafinfo, _setleafinfo!, _leafinfotype, _gettreeinfo @@ -77,7 +81,7 @@ export _getconnections, _addconnection!, _removeconnection! export MatchNodeType, MatchNodeTypes, PreferNodeObjects, _prefernodeobjects # AbstractBranch methods -export _src, _dst, _getlength +export _src, _dst, _getlength, _haslength, _conn, _conns export MatchBranchType, PreferBranchObjects, _preferbranchobjects export MatchBranchNodeType @@ -88,17 +92,16 @@ end include("Interface.jl") # AbstractTree methods -export ntrees, gettrees, nroots, getroots, getroot +export ntrees, gettrees, nroots, getroots, getroot, gettree export treenametype, gettreenames, gettreename #, getonetree #unimplemented -export roottype, nodetype, nodedatatype, nodenametype +export treetype, roottype, nodetype, nodedatatype, nodenametype export branchtype, branchdatatype, branchnametype export createbranch!, deletebranch! export createnode!, createnodes!, deletenode! -export getnodenames, getnodename, hasnode, getnode, getnodes -export getbranchnames, getbranchname, hasbranch, getbranch, getbranches +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 getparent, getancestors #, hasparent # unimplemented -export getchildren, getdescendants #, haschildren # unimplemented export validate!, traversal, branchdims @deprecate addnode! createnode! @@ -112,15 +115,17 @@ export validate!, traversal, branchdims # AbstractTree / AbstractNode methods export isleaf, isroot, isinternal, isunattached -export indegree, outdegree, hasinbound, getinbound, getoutbounds +export degree, indegree, outdegree, hasinbound, getconnections, getinbound, getoutbounds export hasoutboundspace, hasinboundspace -export getleafnames, getleaves, nleaves, nnodes, nbranches export getleafinfo, setleafinfo!, leafinfotype export getnodedata, setnodedata! +export getparent, getancestors #, hasparent # unimplemented +export getchildren, getdescendants #, haschildren # unimplemented +export getsiblings export hasheight, getheight, setheight! # AbstractTree / AbstractBranch methods -export src, dst, getlength +export src, dst, getlength, haslength, conn, conns export hasrootheight, getrootheight, setrootheight! #, clearrootheight! #unimplemented export getbranchdata, setbranchdata! # export getrootdistance # unimplemented @@ -189,12 +194,15 @@ export distance, distances, heighttoroot, heightstoroot # Path into package path(path...; dir::String = "test") = joinpath(@__DIR__, "..", dir, path...) -using Requires +# This symbol is only defined on Julia versions that support extensions +if !isdefined(Base, :get_extension) + using Requires +end + +@static if !isdefined(Base, :get_extension) function __init__() - @require RCall="6f49c342-dc21-5d91-9882-a32aef131414" begin - println("Creating Phylo RCall interface...") - include("rcall.jl") - end + @require RCall="6f49c342-dc21-5d91-9882-a32aef131414" include("../ext/PhyloRCallExt.jl") +end end end # module diff --git a/src/TreeSet.jl b/src/TreeSet.jl index db84d8a8..30f22daf 100644 --- a/src/TreeSet.jl +++ b/src/TreeSet.jl @@ -1,3 +1,8 @@ +""" + TreeSet + +A collection of trees with the same tips. +""" mutable struct TreeSet{LABEL, RT, NL, N, B, TREE <: AbstractTree{OneTree, RT, NL, N, B}} <: AbstractTree{ManyTrees, RT, NL, N, B} diff --git a/src/newick.jl b/src/newick.jl index eca7a1f7..e4d0665a 100644 --- a/src/newick.jl +++ b/src/newick.jl @@ -366,13 +366,30 @@ function parsenewick(tokens::Tokenize.Lexers.Lexer, ::Type{TREE}) where end end +""" + parsenewick(io::IOBuffer, ::Type{TREE}) where TREE <: AbstractTree + +Parse an IOBuffer containing a newick tree and convert into a phylogenetic +tree of type TREE <: AbstractTree +""" parsenewick(io::IOBuffer, ::Type{TREE}) where TREE <: AbstractTree = parsenewick(tokenize(io), TREE) +""" + parsenewick(io::String, ::Type{TREE}) where TREE <: AbstractTree + +Parse a String containing a newick tree and convert into a phylogenetic +tree of type TREE <: AbstractTree +""" parsenewick(s::String, ::Type{TREE}) where TREE <: AbstractTree = parsenewick(IOBuffer(s), TREE) +""" + parsenewick(io::IOStream, ::Type{TREE}) where TREE <: AbstractTree +Parse an IOStream containing a newick tree and convert into a phylogenetic +tree of type TREE <: AbstractTree +""" function parsenewick(ios::IOStream, ::Type{TREE}) where TREE <: AbstractTree buf = IOBuffer() print(buf, read(ios, String)) @@ -380,6 +397,12 @@ function parsenewick(ios::IOStream, ::Type{TREE}) where TREE <: AbstractTree return parsenewick(buf, TREE) end +""" + parsenewick(inp) + +Parse some input containing a newick tree and convert into a phylogenetic +tree of type RootedTree +""" parsenewick(inp) = parsenewick(inp, RootedTree) function parsetaxa(token, state, tokens, taxa) @@ -583,7 +606,7 @@ end function parsenexus(tokens::Tokenize.Lexers.Lexer, ::Type{TREE}) where {RT, NL, N, B, - TREE <: AbstractTree{OneTree, RT, NL, N, B}} + TREE <: AbstractTree{OneTree, RT, NL, N, B}} result = iterateskip(tokens) result === nothing && return nothing token, state = result @@ -598,15 +621,32 @@ function parsenexus(tokens::Tokenize.Lexers.Lexer, end end +""" + parsenexus(io::IOBuffer, ::Type{TREE}) where TREE <: AbstractTree + +Parse an IOBuffer containing a nexus tree and convert into a phylogenetic +tree of type TREE <: AbstractTree +""" parsenexus(io::IOBuffer, ::Type{TREE}) where TREE <: AbstractTree = parsenexus(tokenize(io), TREE) -function parsenexus(ios::IOStream, ::Type{TREE}) where {RT, NL, N, B, - TREE <: AbstractTree{OneTree, RT, NL, N, B}} +""" + parsenexus(io::IOStream, ::Type{TREE}) where TREE <: AbstractTree + +Parse an IOStream containing a nexus tree and convert into a phylogenetic +tree of type TREE <: AbstractTree +""" +function parsenexus(ios::IOStream, ::Type{TREE}) where TREE <: AbstractTree buf = IOBuffer() print(buf, read(ios, String)) seek(buf, 0) return parsenexus(buf, TREE) end +""" + parsenexus(inp) + +Parse some input containing a nexus tree and convert into a phylogenetic +tree of type RootedTree +""" parsenexus(inp) = parsenexus(inp, RootedTree) diff --git a/src/rand.jl b/src/rand.jl index 1608e61b..207c0ccf 100644 --- a/src/rand.jl +++ b/src/rand.jl @@ -371,7 +371,7 @@ using `@enum`), and is the second argument to the constructor: function DiscreteTrait(tree::AbstractTree, ttype::Type{<:Enum}, transition_matrix::AbstractMatrix{Float64}, - trait::String = "\$ttype") + trait::String) The transition matrix holds transition rates from row to column (so row sums must be zero), and the transition probabilities in a branch @@ -460,7 +460,7 @@ created using `@enum`), and is the second argument to the constructor: function DiscreteTrait(tree::AbstractTree, ttype::Type{<:Enum}, transition_rate::Number, - trait::String = "\$ttype") + trait::String) The transition matrix holds transition rates from row to column (so row sums must be zero), and the transition probabilities in a branch diff --git a/src/show.jl b/src/show.jl index 4c04dd5c..38b748e8 100644 --- a/src/show.jl +++ b/src/show.jl @@ -2,35 +2,32 @@ using Phylo using Phylo.API using Printf -function show(io::IO, object::Tuple{<: AbstractTree, <: AbstractNode}, - n::String = "") - node = "node" - if !isempty(n) - node *= " $n" - end - if !hasinbound(object[1], object[2]) - if outdegree(object[1], object[2]) > 0 - blank = repeat(" ", length(" [root $node]") + (isempty(n) ? 0 : 1)) - for (i, bn) in zip(Base.OneTo(_outdegree(object[1], object[2])), - getoutbounds(object[1], object[2])) - b = typeof(bn) <: Number ? "$bn" : "\"$bn\"" - if outdegree(object[1], object[2]) == 1 - print(io, "[root $node]-->[branch $b]") +function show(io::IO, object::NamedTuple{(:tree, :node), Tuple{T, N}}) where + {RT <: Rooted, NL, T <: AbstractTree{OneTree, RT, NL}, N} + node = getnodename(object.tree, object.node) + od = outdegree(object.tree, object.node) + if !hasinbound(object.tree, object.node) + if od > 0 + blank = repeat(" ", length("[root $node]")) + for (i, bn) in enumerate(getbranchname.(Ref(object.tree), getoutbounds(object.tree, object.node))) + b = typeof(bn) <: Number ? "branch $bn" : "\"$bn\"" + if od == 1 + print(io, "[root $node] --> (branch $b)") elseif get(io, :compact, false) if i == 1 - print(io, "[root $node]-->[branches $b") - elseif i < outdegree(object[1], object[2]) - print(io, ", $b") + print(io, "[root $node] --> (branches $b,") + elseif i < od + print(io, "$b,") else - print(io, " and $b]") + print(io, "and $b)") end else # multiline view if i == 1 - println(io, "[root $node]-->[branch $b]") - elseif i < outdegree(object[1], object[2]) - println(io, "$blank-->[branch $b]") + println(io, "[root $node] --> (branch $b)") + elseif i < od + println(io, "$blank --> (branch $b)") else - print(io, "$blank-->[branch $b]") + print(io, "$blank --> (branch $b)") end end end @@ -38,37 +35,31 @@ function show(io::IO, object::Tuple{<: AbstractTree, <: AbstractNode}, print(io, "[unattached $node]") end else # hasinbound - inb = typeof(getinbound(object[1], object[2])) <: Number ? - "$(_getinbound(object[1], object[2]))" : - "\"$(_getinbound(object[1], object[2]))\"" - if outdegree(object[1], object[2]) == 0 - print(io, "[branch $inb]-->[leaf $node]") - elseif hasinbound(object[1], object[2]) - blank = repeat(" ", - length(" [branch $inb]-->[internal $node]") + - (isempty(n) ? 0 : 1)) - for (i, bn) in zip(Base.OneTo(_outdegree(object[1], object[2])), - getoutbounds(object[1], object[2])) + bn = getbranchname(object.tree, getinbound(object.tree, object.node)) + inb = typeof(bn) <: Number ? "$bn" : "\"bn\"" + if od == 0 + print(io, "(branch $inb) --> [leaf $node]") + else + blank = repeat(" ", length("(branch $inb) --> [internal $node]")) + for (i, bn) in enumerate(getbranchname.(Ref(object.tree), getoutbounds(object.tree, object.node))) b = typeof(bn) <: Number ? "$bn" : "\"$bn\"" - if outdegree(object[1], object[2]) == 1 - print(io, "[branch $inb]-->[internal $node]-->[branch $b]") + if od == 1 + print(io, "(branch $inb) --> [internal $node] --> (branch $b)") elseif get(io, :compact, false) if i == 1 - print(io, "[branch $inb]-->[internal $node]-->" * - "[branches $b") - elseif i < outdegree(object[1], object[2]) - print(io, ", $b") + print(io, "(branch $inb) --> [internal $node] --> (branches $b,") + elseif i < od + print(io, "$b,") else - print(io, " and $b]") + print(io, "and $b)") end else # multiline view if i == 1 - println(io, "[branch $inb]-->[internal $node]-->" * - "[branch $b]") - elseif i < outdegree(object[1], object[2]) - println(io, "$blank-->[branch $b]") + println(io, "(branch $inb) --> [internal $node] --> (branch $b)") + elseif i < od + println(io, "$blank --> (branch $b)") else - print(io, "$blank-->[branch $b]") + print(io, "$blank --> (branch $b)") end end end @@ -76,27 +67,18 @@ function show(io::IO, object::Tuple{<: AbstractTree, <: AbstractNode}, end end -function show(io::IO, p::Pair{NT, N}) where {N <: AbstractNode, NT} - n = NT <: Number ? "$(p[1])" : "\"$(p[1])\"" - show(io, p[2], "$n") -end - -function show(io::IO, object::Tuple{<: AbstractTree{OneTree, RT, NL}, - <: AbstractBranch{RT, NL}}) where {RT, NL} - source = NL <: Number ? "$(_src(object[1], object[2]))" : - "\"$(_src(object[1], object[2]))\"" - destination = NL <: Number ? "$(_dst(object[1], object[2]))" : - "\"$(_dst(object[1], object[2]))\"" - println(io, "[node $source]-->[$(_getlength(object[1], object[2])) " * - "length branch]--> [node $destination]") -end - -function show(io::IO, p::Pair{BT, B}) where {BT, B <: AbstractBranch} - NT = typeof(_src(p[2])) - source = NT <: Number ? "$(_src(p[2]))" : "\"$(_src(p[2]))\"" - destination = NT <: Number ? "$(_dst(p[2]))" : "\"$(_dst(p[2]))\"" - branch = BT <: Number ? "$(p[1])" : "\"$(p[1])\"" - println(io, "[node $source]-->[$(_getlength(p[2])) length branch $branch]-->[node $destination]") +function show(io::IO, object::NamedTuple{(:tree, :branch), Tuple{T, B}}) where + {RT <: Rooted, NL, T <: AbstractTree{OneTree, RT, NL}, B} + source = NL <: Number ? "node $(getnodename(object.tree, src(object.tree, object.branch)))" : + "\"$(getnodename(object.tree, src(object.tree, object.branch)))\"" + destination = NL <: Number ? "node $(getnodename(object.tree, dst(object.tree, object.branch)))" : + "\"$(getnodename(object.tree, dst(object.tree, object.branch)))\"" + if haslength(object.tree, object.branch) + print(io, "[$source] --> ($(getlength(object.tree, object.branch)) " * + "length branch $(getbranchname(object.tree, object.branch))) --> [$destination]") + else + print(io, "[$source] --> (branch $(getbranchname(object.tree, object.branch))) --> [$destination]") + end end function showsimple(io::IO, object::TreeSet) @@ -113,7 +95,7 @@ end function show(io::IO, object::TreeSet) showsimple(io, object) - tn = sort(collect(_gettreenames(object))) + tn = sort(collect(gettreenames(object))) index = 0 for name in tn index += 1 @@ -126,30 +108,35 @@ function show(io::IO, object::TreeSet) end end -function showsimple(io::IO, object::TREE) where {TREE <: AbstractTree} - println(io, "$TREE with $(_nleaves(object)) tips, " * - "$(_nnodes(object)) nodes and " * - "$(_nbranches(object)) branches.") +function showsimple(io::IO, object::TREE) where TREE <: AbstractTree + print(io, "$TREE with $(nleaves(object)) tips and $(nroots(object)) roots. ") ln = getleafnames(object) if length(ln) < 10 - println(io, "Leaf names are " * join(ln, ", ", " and ")) + print(io, "Leaf names are " * join(ln, ", ", " and ")) else - println(io, "Leaf names are " * join(ln[1:5], ", ") * - ", ... [$(length(ln) - 6) omitted] ... and $(ln[end])") + print(io, "Leaf names are " * join(ln[1:5], ", ") * + ", ... [$(length(ln) - 6) omitted] ... and $(ln[end])") end end function show(io::IO, object::AbstractTree) showsimple(io, object) - if !get(io, :compact, true) - println(io, "Nodes:") - println(io, [(object, node) for node in _getnodes(object)]) - println(io, "Branches:") - println(io, [(object, branch) for branch in _getbranches(object)]) - if _nodedatatype(TREE) !== Nothing + if get(io, :compact, false) + println("$(nnodes(object)) nodes and $(nbranches(object)) branches.") + else + println(io) + println(io, "$(nnodes(object)) nodes:") + for node in getnodes(object) + println(io, (tree=object, node=node)) + end + println(io, "$(nbranches(object)) branches:") + for branch in getbranches(object) + println(io, (tree=object, branch=branch)) + end + if nodedatatype(typeof(object)) ≢ Nothing println(io, "Node records:") - println(io, Dict(name => _getnodedata(object, name) - for name in _getnodenames(object))) + println(io, Dict(name => getnodedata(object, name) + for name in getnodenames(object))) end end end diff --git a/src/trim.jl b/src/trim.jl index 87008e09..ebd93732 100644 --- a/src/trim.jl +++ b/src/trim.jl @@ -4,6 +4,7 @@ using Phylo.API """ getinternalnodes(t::AbstractTree) + Function to retrieve only the internal nodes from a tree, `t`, which does not include tips or root. diff --git a/test/Project.toml b/test/Project.toml deleted file mode 100644 index 8829214a..00000000 --- a/test/Project.toml +++ /dev/null @@ -1,6 +0,0 @@ -[deps] -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -IterableTables = "1c8ee90f-4401-5389-894e-7a04a3dc0f4d" -RCall = "6f49c342-dc21-5d91-9882-a32aef131414" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/test_rcall.jl b/test/ext_PhyloRCallExt.jl similarity index 100% rename from test/test_rcall.jl rename to test/ext_PhyloRCallExt.jl diff --git a/test/run_rcall.jl b/test/run_rcall.jl index e6c38b48..077bc114 100644 --- a/test/run_rcall.jl +++ b/test/run_rcall.jl @@ -60,18 +60,22 @@ global skipR = !rcopy(R"require(ape)") end @testset "Testing reading in newick trees from disk" begin - jtree = open(parsenewick, Phylo.path("H1N1.newick")) - rtree = rcall(Symbol("read.tree"), Phylo.path("H1N1.newick")) - @test rcopy(rcall(Symbol("all.equal"), jtree, rtree)) + if nodedatatype(TreeType) <: Dict + jtree = open(io -> parsenewick(io, TreeType), Phylo.path("H1N1.newick")) + rtree = rcall(Symbol("read.tree"), Phylo.path("H1N1.newick")) + @test rcopy(rcall(Symbol("all.equal"), jtree, rtree)) + end end @testset "Testing reading in nexus trees from disk" begin - jts = open(parsenexus, Phylo.path("H1N1.trees")) - rtree1 = R"read.nexus($(Phylo.path(\"H1N1.trees\")))$TREE1" - jtree1 = jts["TREE1"] - @test rcopy(rcall(Symbol("all.equal"), jtree1, rtree1)) - @test "H1N1_A_MIYAGI_3_2000" ∈ nodenameiter(jtree1) - @test collect(keys(gettreeinfo(jts)["TREE1"])) == ["lnP"] + if nodedatatype(TreeType) <: Dict + jts = open(io -> parsenexus(io, TreeType), Phylo.path("H1N1.trees")) + rtree1 = R"read.nexus($(Phylo.path(\"H1N1.trees\")))$TREE1" + jtree1 = jts["TREE1"] + @test rcopy(rcall(Symbol("all.equal"), jtree1, rtree1)) + @test "H1N1_A_MIYAGI_3_2000" ∈ nodenameiter(jtree1) + @test collect(keys(gettreeinfo(jts)["TREE1"])) == ["lnP"] + end end end end diff --git a/test/runtests.jl b/test/runtests.jl index 7429e67c..315b67d4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,24 +1,18 @@ +using Random using Test +using Phylo # Identify files in test/ that are testing matching files in src/ # - src/Source.jl will be matched by test/test_Source.jl - -filebase = map(file -> replace(file, r"(.*).jl$" => s"\1"), - filter(file -> occursin(r".*\.jl$", file), - readdir("../src"))) -testbase = map(file -> replace(file, r"test_(.*).jl$" => s"\1"), - filter(str -> occursin(r"^test_.*\.jl$", str), readdir())) - -println() -@info "Running tests for files:" -for t in testbase - println(" = $t.jl") +filebase = String[] +for (root, dirs, files) in walkdir("../src") + append!(filebase, + map(file -> replace(file, r"(.*).jl" => s"\1"), + filter(file -> occursin(r".*\.jl", file), files))) end -println() -@testset "* Testing $t.jl" for t in testbase - include("test_$t.jl") -end +testbase = map(file -> replace(file, r"test_(.*).jl" => s"\1"), + filter(str -> occursin(r"^test_.*\.jl$", str), readdir())) # Identify tests with no matching file superfluous = filter(f -> f ∉ filebase, testbase) @@ -32,16 +26,84 @@ if length(superfluous) > 0 end # Identify files with no matching test -missing = filter(f -> f ∉ testbase, filebase) -if length(missing) > 0 +notest = filter(f -> f ∉ testbase, filebase) +if length(notest) > 0 println() @info "Potentially missing tests:" - for f in missing + for f in notest println(" - $f.jl") end println() end +# Identify files in test/ that are testing matching files in ext/ +# - ext/SourceExt.jl will be matched by test/ext_SourceExt.jl +filebase = String[] +for (root, dirs, files) in walkdir("../ext") + append!(filebase, + map(file -> replace(file, r"(.*).jl" => s"\1"), + filter(file -> occursin(r".*\.jl", file), files))) +end + +extbase = map(file -> replace(file, r"ext_(.*).jl" => s"\1"), + filter(str -> occursin(r"^ext_.*\.jl$", str), readdir())) + +# Identify tests with no matching file +superfluous = filter(f -> f ∉ filebase, extbase) +if length(superfluous) > 0 + println() + @info "Potentially superfluous extension tests:" + for f in superfluous + println(" + $f.jl") + end + println() +end + +# Identify files with no matching test +notest = filter(f -> f ∉ extbase, filebase) +if length(notest) > 0 + println() + @info "Potentially missing extension tests:" + for f in notest + println(" - $f.jl") + end + println() +end + +# Seed RNG to make tests reproducible +Random.seed!(1234) + +@testset "Phylo.jl" begin + @test isfile(Phylo.path("runtests.jl")) + println() + @info "Running tests for files:" + for t in testbase + println(" = $t.jl") + end + println() + + @info "Running tests..." + @testset for t in testbase + fn = "test_$t.jl" + println(" * Testing $t.jl ...") + include(fn) + end + + println() + @info "Running tests for extensions:" + for t in extbase + println(" = $t.jl") + end + println() + + @info "Running extension tests..." + @testset for t in extbase + fn = "ext_$t.jl" + println(" * Testing $t.jl extension...") + include(fn) + end +end + # Identify files that are cross-validating results against other packages # test/pkg_Package.jl should validate results against the Package package @@ -50,13 +112,17 @@ pkgbase = map(file -> replace(file, r"pkg_(.*).jl$" => s"\1"), readdir())) if length(pkgbase) > 0 - @info "Running cross-validation against:" - for p in pkgbase - println(" = $p") - end - println() + @info "Cross validation packages:" + @testset begin + for p in pkgbase + println(" = $p") + end + println() - @testset " * Validating against $p" for p in pkgbase - include("pkg_$p.jl") + @testset for p in pkgbase + fn = "pkg_$p.jl" + println(" * Validating $p.jl ...") + include(fn) + end end end diff --git a/test/test_Interface.jl b/test/test_Interface.jl index 2c33d4a5..582ef59d 100644 --- a/test/test_Interface.jl +++ b/test/test_Interface.jl @@ -4,16 +4,29 @@ using Phylo using DataFrames using Test +matchbranch = true + +@testset "Parsing multi-root trees" begin + jts = open(parsenexus, Phylo.path("H1N1.trees")) + @test ntrees(jts) == 2 + @test gettreeinfo(jts, "TREE2") ≡ gettreeinfo(jts)["TREE2"] + @test all(values(nroots(jts)) .== nroots(jts, "TREE1")) + @test getroot(jts)["TREE1"] ≡ getroot(jts, "TREE1") + @test getroots(jts)["TREE1"] ≡ getroots(jts, "TREE1") +end + @testset "Build and tear down trees" begin @testset "For $TreeType" for TreeType in [NamedTree, NamedBinaryTree, BinaryTree{ManyRoots, DataFrame, Vector{Float64}}, RootedTree, ManyRootTree] + @test treetype(TreeType) == OneTree species = ["Dog", "Cat", "Human", "Potato", "Apple"] tree = TreeType(species) othernodes = ["Some 1", "Some 2"] nodes = createnodes!(tree, othernodes) + @test !((roottype(TreeType) ≡ ManyRoots) ⊻ validate!(tree)) @test [getnodename(tree, node) for node in nodes] == othernodes extra = [getnodename(tree, node) for node in createnodes!(tree, 1)] @test isa(extra[1], String) @@ -36,7 +49,12 @@ using Test name != node && name ∉ getdescendants(tree, node) && name ∉ species, allnodes) - getbranch(tree, createbranch!(tree, first(itr), node)) + global matchbranch = !matchbranch + if matchbranch + getbranch(tree, createbranch!(tree, getnode(tree, first(itr)), getnode(tree, node))) + else + getbranch(tree, createbranch!(tree, getnodename(tree, first(itr)), getnodename(tree, node))) + end end @test_nowarn getroot(tree) branchnames = [getbranchname(tree, branch) for branch in branches] @@ -49,7 +67,7 @@ using Test node1 = getnode(tree, species[1]) @test !isunattached(tree, node1) && hasoutboundspace(tree, node1) && !hasinboundspace(tree, node1) && - outdegree(tree, node1) == 0 && indegree(tree, node1) == 1 + outdegree(tree, node1) == 0 && indegree(tree, node1) == 1 && degree(tree, node1) == 1 @test hasbranch(tree, b) @test hasbranch(tree, bn) @test getnodename(tree, dst(tree, getbranch(tree, b))) == species[1] @@ -60,15 +78,25 @@ using Test hasoutboundspace(tree, species[1]) && hasinboundspace(tree, species[1]) && outdegree(tree, species[1]) == 0 && - indegree(tree, species[1]) == 0 + indegree(tree, species[1]) == 0 && + degree(tree, node1) == 0 @test_throws Exception getbranch(tree, b) branches = [branch for branch in branches if branch != b] @test Set(branches) == Set(getbranches(tree)) b3 = getinbound(tree, species[2]) source = src(tree, b3) destination = dst(tree, b3) + @test Set(conns(tree, b3)) == Set([source, conn(tree, b3, source)]) @test deletebranch!(tree, b3) branches = [branch for branch in branches if branch != b3] + createbranch!(tree, who, species[1]) + deletebranch!(tree, who, species[1]) + @test isunattached(tree, species[1]) && + hasoutboundspace(tree, species[1]) && + hasinboundspace(tree, species[1]) && + outdegree(tree, species[1]) == 0 && + indegree(tree, species[1]) == 0 && + degree(tree, node1) == 0 b3 = createbranch!(tree, who, species[1]) @test who == getnodename(tree, src(tree, b3)) spparent = getparent(tree, getnode(tree, species[1])) @@ -84,6 +112,14 @@ using Test @test_nowarn createbranch!(tree, who, species[1]) @test hasnode(tree, species[1]) @test validate!(tree) + NT = typeof(node1) + @test NT ≡ String || nodenametype(NT) ≡ String + @test NT ≡ String || branchnametype(NT) ≡ Int + @test NT ≡ String || roottype(NT) ≡ roottype(TreeType) + BT = typeof(b) + @test nodenametype(BT) ≡ String + @test branchnametype(BT) ≡ Int + @test roottype(BT) ≡ roottype(TreeType) @test all(isleaf(tree, node) for node in species) @test all((!isroot(tree, node) & !isunattached(tree, node) & !isinternal(tree, node)) for node in species) @@ -92,7 +128,11 @@ using Test Set(getnodename(tree, dst(tree, branch)) for branch in getbranches(tree)) == Set(getnodenames(tree)) createnode!(tree) - @test (roottype(TreeType) == OneRoot) ⊻ validate!(tree) + @test treenametype(TreeType) ≡ Int ? gettreename(tree) == 1 : gettreename(tree) == "Tree" + @test (roottype(TreeType) ≡ OneRoot) ⊻ validate!(tree) + @test gettree(tree) ≡ tree + @test length(getnodes(tree)) == nnodes(tree) + @test ninternal(tree) == nnodes(tree) - nleaves(tree) end end diff --git a/test/test_plot.jl b/test/test_plot.jl new file mode 100644 index 00000000..d09fc243 --- /dev/null +++ b/test/test_plot.jl @@ -0,0 +1,12 @@ +module TestPlots +using Test + +using Phylo +using Plots + +@testset "Plots" begin + tree = open(parsenewick, Phylo.path("hummingbirds.tree")) + @test all(getproperty.([plot(tree), plot(tree, treetype = :fan)], :n) .== 1) +end + +end diff --git a/test/test_show.jl b/test/test_show.jl index c106cbc7..b906c81d 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -23,7 +23,11 @@ a = IOBuffer() @test_nowarn show(a, ts) ps = parsenewick("((,),(,,));", TreeType) @test_nowarn show(a, ps) + @test_nowarn show(a, [ps]) @test_nowarn show(a, first(nodeiter(ps))) @test_nowarn show(a, first(branchiter(ps))) + @test_nowarn show(a, (tree=ps, node=first(getnodes(ps)))) + @test_nowarn show(a, (tree=ps, node=getparent(ps, first(getleaves(ps))))) + @test_nowarn show(a, (tree=ps, branch=first(getbranches(ps)))) end end