diff --git a/docs/src/tutorials/2.descendants_ancestors_filters.md b/docs/src/tutorials/2.descendants_ancestors_filters.md index f1f0a73..5ba2a50 100644 --- a/docs/src/tutorials/2.descendants_ancestors_filters.md +++ b/docs/src/tutorials/2.descendants_ancestors_filters.md @@ -133,10 +133,10 @@ descendants(mtg, :Length, scale = 3) ### Filter by symbol -If we need only the leaves, we would filter by their symbol (*i.e.* "Leaf"): +If we need only the leaves, we would filter by their symbol (*i.e.* :Leaf): ```@example usepkg -descendants(mtg, :Length, symbol = "Leaf") +descendants(mtg, :Length, symbol = :Leaf) ``` ### Filter by anything @@ -156,14 +156,14 @@ descendants(mtg, :Length, filter_fun = x -> x[:Width] === nothing ? false : x[:W Because `filter_fun` takes a node as input, we can even filter on the node's parent. Let's say for example we want the values for the :Length, but only for the nodes that are children of a an Internode that follows another node: ```@example usepkg -descendants(mtg, :Length, filter_fun = node -> !isroot(node) && symbol(parent(node)) == "Internode" && link(parent(node)) == "<") +descendants(mtg, :Length, filter_fun = node -> !isroot(node) && symbol(parent(node)) == :Internode && link(parent(node)) == :<) ``` In this example it returns only one value, because there is only one node that corresponds to this criteria: The Leaf with id 7. We could apply the same kind of filtering on the node's children, or any combination of topological information and attributes. -Note that we first test if the node is not the root node, because the root node does not have a parent. We then test if the parent's symbol is "Internode" and if the link is "<". +Note that we first test if the node is not the root node, because the root node does not have a parent. We then test if the parent's symbol is :Internode and if the link is :<. ### Filter helpers diff --git a/docs/src/tutorials/3.transform_mtg.md b/docs/src/tutorials/3.transform_mtg.md index 4f825bb..2b573c2 100644 --- a/docs/src/tutorials/3.transform_mtg.md +++ b/docs/src/tutorials/3.transform_mtg.md @@ -213,7 +213,7 @@ transform!(mtg, node -> isleaf(node) ? println(node_id(node)," is a leaf") : not We can also use this form to mutate the MTG of a node (which is not possible with Form 2). Here's an example where we change the "Internode" symbol into "I": ```@example usepkg -transform!(mtg, node -> symbol!(node, "I"), symbol = "Internode") +transform!(mtg, node -> symbol!(node, :I), symbol = :Internode) mtg ``` @@ -255,7 +255,7 @@ DataFrame(mtg_select) [`transform!`](@ref) and [`select!`](@ref) use [`traverse!`](@ref) under the hood to apply a function call to each node of an MTG. [`traverse!`](@ref) is just a little bit less easy to use as it only accepts Form 4. We can obtain the exact same results as the last example of [`transform!`](@ref) using the same call with [`traverse!`](@ref). Let's change the `Leaf` symbol into `L`: ```@example usepkg -traverse!(mtg, node -> symbol!(node, "L"), symbol = "Leaf") +traverse!(mtg, node -> symbol!(node, :L), symbol = :Leaf) mtg ``` @@ -275,7 +275,7 @@ end For users coming from R, we also provide the `@mutate_mtg!` macro that is similar to [`transform!`](@ref) but uses a more `tidyverse`-alike syntax. All values coming from the MTG node must be preceded by a `node.`, as with the `.data$` in the `tidyverse`. The names of the attributes are shortened to just `node.attr_name` instead of `node_attributes(node).attr_name` though. Here's an example usage: ```@example usepkg -@mutate_mtg!(mtg, volume = π * 2 * node.Length, symbol = "I") +@mutate_mtg!(mtg, volume = π * 2 * node.Length, symbol = :I) ``` We see that we first name the new attribute and assign the result of the computation. Constants are provided as is, and values coming from the nodes are prefixes by `node.`. diff --git a/docs/src/tutorials/6.add_remove_nodes.md b/docs/src/tutorials/6.add_remove_nodes.md index 353a713..c8d1ae5 100644 --- a/docs/src/tutorials/6.add_remove_nodes.md +++ b/docs/src/tutorials/6.add_remove_nodes.md @@ -100,7 +100,7 @@ Those functions use a `NodeMTG` (or `MutableNodeMTG`), and automatically: ```@example usepkg mtg_2 = deepcopy(mtg) -insert_parent!(mtg_2, NodeMTG("/", "Scene", 0, 0)) +insert_parent!(mtg_2, NodeMTG(:/, :Scene, 0, 0)) mtg_2 = get_root(mtg_2) ``` @@ -114,7 +114,7 @@ insert_parent!( mtg_2, node -> ( link = link(node), - symbol = "Scene", + symbol = :Scene, index = index(node), scale = scale(node) - 1 ) @@ -147,7 +147,7 @@ mtg_2 = deepcopy(mtg) insert_child!( mtg_2, - NodeMTG("/", "Axis", 0, 2), + NodeMTG(:/, :Axis, 0, 2), node -> Dict{Symbol, Any}(:length => 2, :area => 0.1) ) @@ -161,7 +161,7 @@ mtg_2 = deepcopy(mtg) insert_child!( mtg_2, - NodeMTG("/", "Axis", 0, 2), + NodeMTG(:/, :Axis, 0, 2), node -> Dict{Symbol, Any}(:total_length => sum(descendants(node, :length, ignore_nothing = true))) ) @@ -265,23 +265,23 @@ For example if we need to insert new Flower nodes as parents of each Leaf, we wo ```@example usepkg mtg_4 = deepcopy(mtg) -template = MutableNodeMTG("+", "Flower", 0, 2) -insert_parents!(mtg_4, template, symbol = "Leaf") +template = MutableNodeMTG(:+, :Flower, 0, 2) +insert_parents!(mtg_4, template, symbol = :Leaf) ``` Similarly, we can add a new child to leaves using [`insert_children!`](@ref): ```@example usepkg -template = MutableNodeMTG("/", "Leaflet", 0, 3) -insert_children!(mtg_4, template, symbol = "Leaf") +template = MutableNodeMTG(:/, :Leaflet, 0, 3) +insert_children!(mtg_4, template, symbol = :Leaf) ``` Usually, the flower is positioned as a sibling of the leaf though. To do so, we can use [`insert_siblings!`](@ref): ```@example usepkg mtg_5 = deepcopy(mtg) -template = MutableNodeMTG("+", "Flower", 0, 2) -insert_siblings!(mtg_5, template, symbol = "Leaf") +template = MutableNodeMTG(:+, :Flower, 0, 2) +insert_siblings!(mtg_5, template, symbol = :Leaf) ``` ### Compute the template on the fly @@ -291,8 +291,8 @@ The template for the `NodeMTG` can also be computed on the fly for more complex ```@example usepkg insert_children!( mtg_5, - node -> if node_id(node) == 3 MutableNodeMTG("/", "Spear", 0, 3) else MutableNodeMTG("/", "Leaflet", 0, 3) end, - symbol = "Leaf" + node -> if node_id(node) == 3 MutableNodeMTG(:/, :Spear, 0, 3) else MutableNodeMTG(:/, :Leaflet, 0, 3) end, + symbol = :Leaf ) ``` @@ -303,9 +303,9 @@ The same is true for the attributes. We can provide them as is: ```@example usepkg insert_siblings!( mtg_5, - MutableNodeMTG("+", "Leaf", 0, 2), + MutableNodeMTG(:+, :Leaf, 0, 2), Dict{Symbol, Any}(:area => 0.1), - symbol = "Leaf" + symbol = :Leaf ) ``` @@ -314,9 +314,9 @@ Or compute them based on the node on which we insert the new nodes. For example ```@example usepkg insert_siblings!( mtg_5, - MutableNodeMTG("+", "Leaf", 0, 2), + MutableNodeMTG(:+, :Leaf, 0, 2), node -> node[:area] === nothing ? nothing : Dict{Symbol, Any}(:area => node[:area] * 2), - symbol = "Leaf" + symbol = :Leaf ) ``` diff --git a/docs/src/tutorials/7.performance_considerations.md b/docs/src/tutorials/7.performance_considerations.md index f02d009..d26a0ef 100644 --- a/docs/src/tutorials/7.performance_considerations.md +++ b/docs/src/tutorials/7.performance_considerations.md @@ -22,6 +22,13 @@ The MTG encoding is the type used to store the MTG information about the node, * By default, MultiScaleTreeGraph.jl uses a mutable encoding ([`MutableNodeMTG`](@ref)), which allows for modifying this information. However, if the user does not need to modify these, it is recommended to use an immutable encoding instead ([`NodeMTG`](@ref)). This will improve performance significantly. +The internal representation of MTG `symbol` and `link` values is based on `Symbol` for faster comparisons and lower repeated allocations. For backward compatibility, string inputs are still accepted everywhere (constructors and filters), but using symbols in performance-critical code is recommended: + +```julia +NodeMTG(:/, :Internode, 1, 2) +traverse(mtg, x -> x, symbol=:Internode, link=:<) +``` + ### Traversal: node caching MultiScaleTreeGraph.jl traverses all nodes by default when performing tree traversal. The traversal is done in a recursive manner so it is performant, but not always as fast as it could be. For example, we could have a very large tree with only two leaves at the top. In this case, we would traverse all nodes in the tree, even though we only need to traverse two nodes. @@ -34,10 +41,10 @@ To improve performance, it is possible to cache any type of `traversal`, includi To cache a traversal, you can use [`cache_nodes!`](@ref). For example, if you want to cache all the **leaf** nodes in the MTG, you can do: ```julia -cache_nodes!(mtg, symbol = "Leaf") +cache_nodes!(mtg, symbol = :Leaf) ``` -This will cache all the nodes with the symbol `"Leaf"` in the MTG. Then, the tree traversal functions will use the cached traversal to iterate over the nodes. +This will cache all the nodes with the symbol `:Leaf` in the MTG. Then, the tree traversal functions will use the cached traversal to iterate over the nodes. !!! tip Tree traversal is *very* fast, so caching nodes is not always necessary. Caching should be used when the traversal is needed **multiple times**, and the traversal is sparse, *i.e.* a lot of nodes are filtered-out. diff --git a/src/compute_MTG/ancestors.jl b/src/compute_MTG/ancestors.jl index 80ac022..3c74c55 100644 --- a/src/compute_MTG/ancestors.jl +++ b/src/compute_MTG/ancestors.jl @@ -15,7 +15,7 @@ Make it a `Symbol` for faster computation time. ## Keyword Arguments - `scale = nothing`: The scale to filter-in (i.e. to keep). Usually a Tuple-alike of integers. -- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Strings. +- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Symbols. - `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Char. - `all::Bool = true`: Return all filtered-in nodes (`true`), or stop at the first node that is filtered out (`false`). @@ -56,8 +56,8 @@ ancestors(leaf_node, :XX, scale = 1, type = Float64) ancestors(leaf_node, :Length, scale = 3, type = Float64) # Filter by symbol: -ancestors(leaf_node, :Length, symbol = "Internode") -ancestors(leaf_node, :Length, symbol = ("Axis","Internode")) +ancestors(leaf_node, :Length, symbol = :Internode) +ancestors(leaf_node, :Length, symbol = (:Axis,:Internode)) ``` """ function ancestors( @@ -71,6 +71,8 @@ function ancestors( recursivity_level=-1, ignore_nothing=false, type::Union{Union,DataType}=Any) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # Check the filters once, and then compute the ancestors recursively using `ancestors_` check_filters(node, scale=scale, symbol=symbol, link=link) @@ -146,6 +148,8 @@ function ancestors( filter_fun=nothing, recursivity_level=-1 ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # Check the filters once, and then compute the ancestors recursively using `ancestors_` check_filters(node, scale=scale, symbol=symbol, link=link) @@ -219,6 +223,8 @@ function ancestors!( ignore_nothing=false, type::Union{Union,DataType}=Any, ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) check_filters(node, scale=scale, symbol=symbol, link=link) filter_fun_ = filter_fun_nothing(filter_fun, ignore_nothing, key) use_no_filter = no_node_filters(scale, symbol, link, filter_fun_) @@ -251,6 +257,8 @@ function ancestors!( filter_fun=nothing, recursivity_level=-1, ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) check_filters(node, scale=scale, symbol=symbol, link=link) use_no_filter = no_node_filters(scale, symbol, link, filter_fun) diff --git a/src/compute_MTG/caching.jl b/src/compute_MTG/caching.jl index c12b180..88b9278 100644 --- a/src/compute_MTG/caching.jl +++ b/src/compute_MTG/caching.jl @@ -44,16 +44,18 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files", mtg = read_mtg(file, Dict) # Cache all leaf nodes: -cache_nodes!(mtg, symbol="Leaf") +cache_nodes!(mtg, symbol=:Leaf) # Cached nodes are stored in the traversal_cache field of the mtg (here, the two leaves): @test MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_c0bffb8cc8a9b075e40d26be9c2cac6349f2a790"] == [get_node(mtg, 5), get_node(mtg, 7)] # Then you can use the cached nodes in a traversal: -traverse(mtg, x -> symbol(x), symbol="Leaf") == ["Leaf", "Leaf"] +traverse(mtg, x -> symbol(x), symbol=:Leaf) == [:Leaf, :Leaf] ``` """ function cache_nodes!(node; scale=nothing, symbol=nothing, link=nothing, filter_fun=nothing, all=true, overwrite=false) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # The cache is already present: if length(node_traversal_cache(node)) != 0 && haskey(node_traversal_cache(node), cache_name(scale, symbol, link, all, filter_fun)) if !overwrite @@ -71,4 +73,4 @@ function cache_nodes!(node; scale=nothing, symbol=nothing, link=nothing, filter_ ) return nothing -end \ No newline at end of file +end diff --git a/src/compute_MTG/check_filters.jl b/src/compute_MTG/check_filters.jl index c8002b9..91a0154 100644 --- a/src/compute_MTG/check_filters.jl +++ b/src/compute_MTG/check_filters.jl @@ -8,12 +8,33 @@ Check if the filters are consistant with the mtg onto which they are applied ```julia check_filters(mtg, scale = 1) check_filters(mtg, scale = (1,2)) -check_filters(mtg, scale = (1,2), symbol = "Leaf", link = "<") +check_filters(mtg, scale = (1,2), symbol = :Leaf, link = :<) ``` """ @inline no_node_filters(scale, symbol, link, filter_fun=nothing) = isnothing(scale) && isnothing(symbol) && isnothing(link) && isnothing(filter_fun) +@inline normalize_symbol_filter(filter::Nothing) = nothing +@inline normalize_symbol_filter(filter::Symbol) = filter +@inline normalize_symbol_filter(filter::AbstractString) = Symbol(filter) +@inline normalize_symbol_filter(filter::Char) = Symbol(filter) +@inline function normalize_symbol_filter(filter::T) where {T<:Union{Tuple,AbstractArray}} + map(normalize_symbol_filter, filter) +end + +@inline normalize_link_filter(filter::Nothing) = nothing +@inline normalize_link_filter(filter::Symbol) = filter +@inline normalize_link_filter(filter::AbstractString) = Symbol(filter) +@inline normalize_link_filter(filter::Char) = Symbol(filter) +@inline function normalize_link_filter(filter::T) where {T<:Union{Tuple,AbstractArray}} + map(normalize_link_filter, filter) +end + +@inline normalize_symbol_allowed(filters::Nothing) = nothing +@inline function normalize_symbol_allowed(filters::T) where {T<:Union{Tuple,AbstractArray}} + map(normalize_symbol_filter, filters) +end + function check_filters(node::Node{N,A}; scale=nothing, symbol=nothing, link=nothing) where {N<:AbstractNodeMTG,A} no_node_filters(scale, symbol, link) && return nothing @@ -24,11 +45,11 @@ function check_filters(node::Node{N,A}; scale=nothing, symbol=nothing, link=noth end if root_node[:symbols] !== nothing - check_filter(N, :symbol, symbol, unique(root_node[:symbols])) + check_filter(N, :symbol, normalize_symbol_filter(symbol), unique(normalize_symbol_allowed(root_node[:symbols]))) end if root_node[:link] !== nothing - check_filter(N, :link, link, ("/", "<", "+")) + check_filter(N, :link, normalize_link_filter(link), (:/, :<, :+)) end return nothing @@ -37,7 +58,11 @@ end function check_filter(nodetype, type::Symbol, filter, filters) if !isnothing(filter) filter_type = fieldtype(nodetype, type) - !(typeof(filter) <: filter_type) && + filter_ok = typeof(filter) <: filter_type + if type == :symbol || type == :link + filter_ok = filter_ok || typeof(filter) <: Union{Symbol,AbstractString,Char} + end + !filter_ok && @warn "The $type argument should be of type $filter_type" if !(filter in filters) @warn "The $type argument should be one of: $filters, and you provided $filter." @@ -58,9 +83,9 @@ end Is a node filtered in ? Returns `true` if the node is kept, `false` if it is filtered-out. """ @inline function is_filtered(node, mtg_scale, mtg_symbol, mtg_link, filter_fun) - - link_keep = isnothing(mtg_link) || is_filtered(mtg_link, link(node)) - symbol_keep = isnothing(mtg_symbol) || is_filtered(mtg_symbol, symbol(node)) + node_mtg_ = node_mtg(node) + link_keep = isnothing(mtg_link) || is_filtered(mtg_link, getfield(node_mtg_, :link)) + symbol_keep = isnothing(mtg_symbol) || is_filtered(mtg_symbol, getfield(node_mtg_, :symbol)) scale_keep = isnothing(mtg_scale) || is_filtered(mtg_scale, scale(node)) filter_fun_keep = isnothing(filter_fun) || filter_fun(node) @@ -75,8 +100,42 @@ end value in filter end -@inline function is_filtered(filter::String, value) - value in (filter,) +@inline function is_filtered(filter::AbstractString, value::Symbol) + Symbol(filter) === value +end + +@inline function is_filtered(filter::AbstractString, value::AbstractString) + filter == value +end + +@inline function is_filtered(filter::AbstractString, value) + filter == value +end + +@inline function is_filtered(filter::Symbol, value::AbstractString) + filter === Symbol(value) +end + +@inline function is_filtered(filter::Symbol, value::Symbol) + filter === value +end + +@inline function is_filtered(filter::Symbol, value) + filter === value +end + +@inline function is_filtered(filter::T, value::Symbol) where {T<:Union{Tuple,AbstractArray}} + for f in filter + is_filtered(f, value) && return true + end + return false +end + +@inline function is_filtered(filter::T, value::AbstractString) where {T<:Union{Tuple,AbstractArray}} + for f in filter + is_filtered(f, value) && return true + end + return false end @inline function is_filtered(filter, value::T) where {T<:Union{Tuple,Array}} diff --git a/src/compute_MTG/delete_nodes.jl b/src/compute_MTG/delete_nodes.jl index aabe9ab..007b46d 100644 --- a/src/compute_MTG/delete_nodes.jl +++ b/src/compute_MTG/delete_nodes.jl @@ -12,8 +12,8 @@ Delete nodes in mtg following filters rules. ## Keyword Arguments (filters) - `scale = nothing`: The scale to delete. Usually a Tuple-alike of integers. -- `symbol = nothing`: The symbol to delete. Usually a Tuple-alike of Strings. -- `link = nothing`: The link with the previous node to delete. Usually a Tuple-alike of Char. +- `symbol = nothing`: The symbol to delete. Usually a Tuple-alike of Symbols. +- `link = nothing`: The link with the previous node to delete. Usually a Tuple-alike of Symbol. - `all::Bool = true`: Continue after the first deletion (`true`), or stop? - `filter_fun = nothing`: Any filtering function taking a node as input, e.g. [`isleaf`](@ref) to decide whether to delete a node or not. @@ -42,9 +42,9 @@ mtg = read_mtg(file) delete_nodes!(mtg, scale = 2) # Will remove all nodes of scale 2 # Delete the leaves: -delete_nodes!(mtg, symbol = "Leaf") +delete_nodes!(mtg, :Leaf) # Delete the leaves and internodes: -delete_nodes!(mtg, symbol = ("Leaf","Internode")) +delete_nodes!(mtg, symbol = (:Leaf,:Internode)) ``` """ function delete_nodes!( @@ -190,7 +190,7 @@ function new_child_link(node, verbose=true) deleted_link = parent(node) |> link child_link = link(node) - if deleted_link == "+" && child_link == "/" + if deleted_link == :+ && child_link == :/ new_child_link = child_link verbose && @warn join( [ @@ -198,9 +198,9 @@ function new_child_link(node, verbose=true) " Keep decomposition, please check if the branching is still correct." ] ) deleted_link child_link new_child_link - elseif deleted_link == "+" && child_link == "<" + elseif deleted_link == :+ && child_link == :< new_child_link = deleted_link - elseif deleted_link == "/" && child_link == "+" + elseif deleted_link == :/ && child_link == :+ new_child_link = child_link verbose && @warn join( [ @@ -208,7 +208,7 @@ function new_child_link(node, verbose=true) " Keep branching, please check if the decomposition is still correct." ] ) deleted_link child_link new_child_link - elseif deleted_link == "/" && child_link == "<" + elseif deleted_link == :/ && child_link == :< new_child_link = deleted_link else new_child_link = child_link diff --git a/src/compute_MTG/descendants.jl b/src/compute_MTG/descendants.jl index e8cbb14..d9fa706 100644 --- a/src/compute_MTG/descendants.jl +++ b/src/compute_MTG/descendants.jl @@ -65,6 +65,8 @@ function descendants( recursivity_level=Inf, ignore_nothing::Bool=false, type::Union{Union,DataType}=Any) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # Check the filters once, and then compute the descendants recursively using `descendants_` check_filters(node, scale=scale, symbol=symbol, link=link) @@ -106,6 +108,8 @@ function descendants( filter_fun=nothing, recursivity_level=Inf, ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # Check the filters once, and then compute the descendants recursively using `descendants_` check_filters(node, scale=scale, symbol=symbol, link=link) @@ -144,7 +148,10 @@ function descendants!( filter_fun=nothing, recursivity_level=Inf, ignore_nothing::Bool=false, + type::Union{Union,DataType}=Any, ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) check_filters(node, scale=scale, symbol=symbol, link=link) filter_fun_ = filter_fun_nothing(filter_fun, ignore_nothing, key) use_no_filter = no_node_filters(scale, symbol, link, filter_fun_) @@ -179,6 +186,8 @@ function descendants!( filter_fun=nothing, recursivity_level=Inf, ) + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) check_filters(node, scale=scale, symbol=symbol, link=link) use_no_filter = no_node_filters(scale, symbol, link, filter_fun) @@ -213,6 +222,8 @@ function descendants!( recursivity_level=Inf, ignore_nothing=false, type::Union{Union,DataType}=Any) where {N,A<:AbstractDict} + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # Check the filters once, and then compute the descendants recursively using `descendants_` check_filters(node, scale=scale, symbol=symbol, link=link) @@ -278,9 +289,10 @@ function descendants_!(node, key, scale, symbol, link, all, filter_fun, val, rec end """ - descendants(node::Node,key,) - descendants(node::Node,) - descendants!(node::Node,key,) + descendants(node::Node,key;) + descendants(node::Node;) + descendants!(node::Node,key;) + descendants!(out::AbstractVector,node::Node,key;) Get attribute values from the descendants of the node (acropetal). The first method returns an array of values, the second an array of nodes that respect the filters, and the third the mutating version of the @@ -288,7 +300,7 @@ first one that caches the results in the mtg. The mutating version (`descendants!`) cache the results in a cached variable named after the hash of the function call. This version is way faster when `descendants` is called repeateadly for the same computation on large trees, but require to clean the chache sometimes -(see [`clean_cache!`](@ref)). It also only works for trees with attributes of subtype of `AbstractDict`. +(see [`clean_cache!`](@ref)). # Arguments @@ -300,8 +312,8 @@ is way faster when `descendants` is called repeateadly for the same computation ## Keyword Arguments - `scale = nothing`: The scale to filter-in (i.e. to keep). Usually a Tuple-alike of integers. -- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Strings. -- `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Char. +- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Symbols. +- `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Symbols. - `all::Bool = true`: Return all filtered-in nodes (`true`), or stop at the first node that is filtered out (`false`). - `self = false`: is the value for the current node needed ? @@ -344,8 +356,8 @@ descendants(mtg, :XEuler, scale = 3, type = Union{Nothing, Float64}) descendants(mtg, :Length, scale = 3, type = Float64) # No `nothing` value here, no need of a union type # Filter by symbol: -descendants(mtg, :Length, symbol = "Leaf") -descendants(mtg, :Length, symbol = ("Leaf","Internode")) +descendants(mtg, :Length, symbol = :Leaf) +descendants(mtg, :Length, symbol = (:Leaf,:Internode)) # Filter by function, e.g. get the values for the leaves only: descendants(mtg, :Width; filter_fun = isleaf) @@ -356,10 +368,10 @@ descendants(mtg, [:Width, :Length]; filter_fun = isleaf) # It is possible to cache the results in the mtg using the mutating version `descendants!` (note the `!` # at the end of the function name): -transform!(mtg, node -> sum(descendants!(node, :Length)) => :subtree_length, symbol = "Internode") +transform!(mtg, node -> sum(descendants!(node, :Length)) => :subtree_length, symbol = :Internode) # Or using `@mutate_mtg!` instead of `transform!`: -@mutate_mtg!(mtg, subtree_length = sum(descendants!(node, :Length)), symbol = "Internode") +@mutate_mtg!(mtg, subtree_length = sum(descendants!(node, :Length)), symbol = :Internode) # The cache is stored in a temporary variable with a name that starts with `_cache_` followed by the SHA # of the function call, *e.g.*: `:_cache_5c1e97a3af343ce623cbe83befc851092ca61c8d`: diff --git a/src/compute_MTG/filter/filter-funs.jl b/src/compute_MTG/filter/filter-funs.jl index 96ab173..a3b73c0 100644 --- a/src/compute_MTG/filter/filter-funs.jl +++ b/src/compute_MTG/filter/filter-funs.jl @@ -17,18 +17,18 @@ function is_segment!(node::Node{N,A}) where {N<:AbstractNodeMTG,A} # first node even if it has only one child. # If there is only one child but it is branching: - if node_mtg(node[1]).link == "+" + if getfield(node_mtg(node[1]), :link) === :+ return false end # If it's a node that branches, set its unique child as the branching node instead: - if node_mtg(node).link == "+" + if getfield(node_mtg(node), :link) === :+ node_MTG = node_mtg(node[1]) node_mtg!(node[1], N("+", node_MTG.symbol, node_MTG.index, node_MTG.scale)) end # If it's a node that decompose ("/"), set its unique child as the decomposing node: - if node_mtg(node).link == "/" + if getfield(node_mtg(node), :link) === :/ node_MTG = node_mtg(node[1]) node_mtg!(node[1], N("/", node_MTG.symbol, node_MTG.index, node_MTG.scale)) end diff --git a/src/compute_MTG/insert_nodes.jl b/src/compute_MTG/insert_nodes.jl index f3cbb37..d085ace 100644 --- a/src/compute_MTG/insert_nodes.jl +++ b/src/compute_MTG/insert_nodes.jl @@ -31,12 +31,12 @@ the children of the inserted node: `insert_generations!` ## Keyword Arguments (filters) - `scale = nothing`: The scale at which to insert. Usually a Tuple-alike of integers. -- `symbol = nothing`: The symbol at which to insert. Usually a Tuple-alike of Strings. -- `link = nothing`: The link with at which to insert. Usually a Tuple-alike of Char. +- `symbol = nothing`: The symbol at which to insert. Usually a Tuple-alike of Symbols. +- `link = nothing`: The link with at which to insert. Usually a Tuple-alike of Symbols. - `all::Bool = true`: Continue after the first insertion (`true`), or stop. - `filter_fun = nothing`: Any function taking a node as input, e.g. [`isleaf`](@ref) to decide on which node the insertion will be based on. - + # Examples ```julia @@ -44,7 +44,7 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))),"test","files","si mtg = read_mtg(file) # Insert new Shoot nodes before all scale 2 nodes: -mtg = insert_parents!(mtg, MultiScaleTreeGraph.MutableNodeMTG("/", "Shoot", 0, 1), scale = 2) +mtg = insert_parents!(mtg, MultiScaleTreeGraph.MutableNodeMTG(:/, :Shoot, 0, 1), scale = 2) mtg ``` @@ -188,12 +188,11 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))),"test","files","si mtg = read_mtg(file) # using a NodeMTG as a template: -MultiScaleTreeGraph.new_node_MTG(mtg, NodeMTG("/", "Leaf", 1, 2)) +MultiScaleTreeGraph.new_node_MTG(mtg, NodeMTG(:/, :Leaf, 1, 2)) # Note that it returns a MutableNodeMTG because `mtg` is using this type instead of a `NodeMTG` # using a NamedTuple as a template: -MultiScaleTreeGraph.new_node_MTG(mtg, (link = "/", symbol = "Leaf", index = 1, scale = 2)) - +MultiScaleTreeGraph.new_node_MTG(mtg, (link = :/, symbol = :Leaf, index = 1, scale = 2)) # using a function that returns a template based on the first child of the node: MultiScaleTreeGraph.new_node_MTG( mtg, @@ -260,7 +259,7 @@ insert_parent!( link = link(node[1]), symbol = symbol(node[1]), index = index(node[1]), - scale = scale(node[1])) + scale = scale(node[1]) ) ) ``` diff --git a/src/compute_MTG/mutation_helpers.jl b/src/compute_MTG/mutation_helpers.jl index a463208..db0b70b 100644 --- a/src/compute_MTG/mutation_helpers.jl +++ b/src/compute_MTG/mutation_helpers.jl @@ -61,7 +61,7 @@ function branching_order_ascend!(node) if isroot(node) return 1 else - if link(node) == "+" + if link(node) == :+ return parent(node)[:branching_order] + 1 else return parent(node)[:branching_order] @@ -81,7 +81,7 @@ function branching_order_descend!(node) end val = maximum(val_child) - if link(node) == "+" + if link(node) == :+ val += 1 end end diff --git a/src/compute_MTG/summary.jl b/src/compute_MTG/summary.jl index 65f31fe..806ee7c 100644 --- a/src/compute_MTG/summary.jl +++ b/src/compute_MTG/summary.jl @@ -4,7 +4,7 @@ Compute the mtg classes based on its content. Usefull after having mutating the mtg nodes. """ function get_classes(mtg) - attributes = traverse(mtg, node -> (SYMBOL=symbol(node), SCALE=scale(node)), type=@NamedTuple{SYMBOL::String, SCALE::Int64}) + attributes = traverse(mtg, node -> (SYMBOL=symbol(node), SCALE=scale(node)), type=@NamedTuple{SYMBOL::Symbol, SCALE::Int64}) attributes = unique(attributes) df = DataFrame(attributes) @@ -95,7 +95,7 @@ function scales(mtg) end function symbols(mtg) - vec = String[] + vec = Symbol[] traverse!(mtg) do node push!(vec, symbol(node)) end diff --git a/src/compute_MTG/transform.jl b/src/compute_MTG/transform.jl index 493b799..59e2190 100644 --- a/src/compute_MTG/transform.jl +++ b/src/compute_MTG/transform.jl @@ -12,8 +12,8 @@ attributes specified by `args...`. - : - `scale = nothing`: The scale to filter-in (i.e. to keep). Usually a Tuple-alike of integers. - - `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Strings. - - `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Char. + - `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Symbols. + - `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Symbols. - `filter_fun = nothing`: Any filtering function taking a node as input, e.g. [`isleaf`](@ref). - `ignore_nothing = false`: filter-out the nodes with `nothing` values for the given attributes used as inputs (apply only to the form :var_name => ...) diff --git a/src/compute_MTG/traverse.jl b/src/compute_MTG/traverse.jl index 462cacf..13f912f 100644 --- a/src/compute_MTG/traverse.jl +++ b/src/compute_MTG/traverse.jl @@ -14,8 +14,8 @@ which is either mutating (use `traverse!`) or not (use `traverse`). - : - `scale = nothing`: The scale to filter-in (i.e. to keep). Usually a Tuple-alike of integers. - - `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Strings. - - `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Char. + - `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Symbols. + - `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Symbols. - `filter_fun = nothing`: Any filtering function taking a node as input, e.g. [`isleaf`](@ref). - `all::Bool = true`: Return all filtered-in nodes (`true`), or stop at the first node that is filtered out (`false`). - `type::Type = Any`: The elements type of the returned array. This can speed-up things. Only available for the non-mutating version. @@ -109,6 +109,8 @@ function traverse!(node::Node, f::Function, args...; scale=nothing, symbol=nothi else g = f end + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) # If the node has already a cache of the traversal, we use it instead of traversing the mtg: cache = node_traversal_cache(node) @@ -163,6 +165,8 @@ function traverse(node::Node, f::Function, args...; scale=nothing, symbol=nothin else g = f end + symbol = normalize_symbol_filter(symbol) + link = normalize_link_filter(link) val = Array{type,1}() # NB: f has to return someting here, if its a mutating function, use traverse! diff --git a/src/conversion/MetaGraph.jl b/src/conversion/MetaGraph.jl index c6bcde2..c45d3f7 100644 --- a/src/conversion/MetaGraph.jl +++ b/src/conversion/MetaGraph.jl @@ -19,7 +19,7 @@ function MetaGraph(g::Node{N,A}) where {N<:AbstractNodeMTG,A} DiGraph(), label_type=Dict{Int64,Int64}(), vertex_data_type=Dict{Int64,Tuple{Int64,A}}(), - edge_data_type=Dict{Tuple{Int64,Int64},String}(), + edge_data_type=Dict{Tuple{Int64,Int64},Symbol}(), graph_data="MTG", weight_function=edata -> 1.0, default_weight=1.0, diff --git a/src/print_MTG/print.jl b/src/print_MTG/print.jl index 0212048..0c580a7 100644 --- a/src/print_MTG/print.jl +++ b/src/print_MTG/print.jl @@ -44,7 +44,7 @@ end Format the printing of the tree according to link: follow or branching """ function get_printing(node::Node; leading::AbstractString="") - node_vec = [link(node) * " " * string(node_id(node)) * ": " * symbol(node)] + node_vec = [string(link(node)) * " " * string(node_id(node)) * ": " * string(symbol(node))] print_below = get_printing_(node; leading="") if print_below !== nothing append!(node_vec, print_below) @@ -64,10 +64,10 @@ function get_printing_(node::Node; leading::AbstractString="") id = node_id(chnode) i += 1 if i != last - to_print = leading * "\u251C\u2500 " * mtg_encoding.link * " " * string(id) * ": " * mtg_encoding.symbol + to_print = leading * "\u251C\u2500 " * string(mtg_encoding.link) * " " * string(id) * ": " * string(mtg_encoding.symbol) new_leading = leading * "\u2502 " else - to_print = leading * "\u2514\u2500 " * mtg_encoding.link * " " * string(id) * ": " * mtg_encoding.symbol + to_print = leading * "\u2514\u2500 " * string(mtg_encoding.link) * " " * string(id) * ": " * string(mtg_encoding.symbol) new_leading = leading * " " end append!(child_vec, [to_print]) diff --git a/src/read_MTG/parse_mtg.jl b/src/read_MTG/parse_mtg.jl index 92b7718..17a7d8a 100644 --- a/src/read_MTG/parse_mtg.jl +++ b/src/read_MTG/parse_mtg.jl @@ -353,8 +353,8 @@ function parse_line_to_node!(tree_dict, l, line, attr_column_start, last_node_co # Instantiating the current node MTG: childMTG = mtg_type( - node_element[1], - symbol, + Symbol(node_element[1]), + Symbol(symbol), node_element[3], scale ) diff --git a/src/types/AbstractNodeMTG.jl b/src/types/AbstractNodeMTG.jl index 211915d..ec9c4b6 100644 --- a/src/types/AbstractNodeMTG.jl +++ b/src/types/AbstractNodeMTG.jl @@ -29,16 +29,38 @@ NodeMTG("<", "Leaf", 2, 0) """ NodeMTG, MutableNodeMTG +@inline function to_mtg_link(link::Symbol) + link +end + +@inline function to_mtg_link(link::AbstractString) + Symbol(link) +end + +@inline function to_mtg_link(link::Char) + Symbol(link) +end + +@inline function to_mtg_symbol(symbol::Symbol) + symbol +end + +@inline function to_mtg_symbol(symbol::AbstractString) + Symbol(symbol) +end + struct NodeMTG <: AbstractNodeMTG - link::String - symbol::String + link::Symbol + symbol::Symbol index::Int scale::Int function NodeMTG(link, symbol, index, scale) + link_ = to_mtg_link(link) + symbol_ = to_mtg_symbol(symbol) @assert scale >= 0 "The scale should be greater than or equal to 0." - @assert link in ["/", "<", "+"] "The link should be one of '/', '<', '+'" - return new(link, symbol, index, scale) + @assert link_ in (:/, :<, :+) "The link should be one of `:/`, `:<`, `:+`" + return new(link_, symbol_, index, scale) end end @@ -47,18 +69,29 @@ function NodeMTG(link, symbol, index::Nothing, scale) end mutable struct MutableNodeMTG <: AbstractNodeMTG - link::String - symbol::String + link::Symbol + symbol::Symbol index::Int scale::Int function MutableNodeMTG(link, symbol, index, scale) + link_ = to_mtg_link(link) + symbol_ = to_mtg_symbol(symbol) @assert scale >= 0 "The scale should be greater than or equal to 0." - @assert link in ["/", "<", "+"] "The link should be one of '/', '<', '+'" - new(link, symbol, index, scale) + @assert link_ in (:/, :<, :+) "The link should be one of ':/', ':<', ':+'" + new(link_, symbol_, index, scale) end end function MutableNodeMTG(link, symbol, index::Nothing, scale) MutableNodeMTG(link, symbol, -9999, scale) +end + +function Base.setproperty!(m::MutableNodeMTG, key::Symbol, value) + if key === :link + return setfield!(m, :link, to_mtg_link(value)) + elseif key === :symbol + return setfield!(m, :symbol, to_mtg_symbol(value)) + end + return setfield!(m, key, value) end \ No newline at end of file diff --git a/src/types/Node.jl b/src/types/Node.jl index 9a7b9e4..6743e3b 100644 --- a/src/types/Node.jl +++ b/src/types/Node.jl @@ -293,7 +293,7 @@ link(node::Node) = getfield(node_mtg(node), :link) Set the symbol of the MTG encoding node. """ -symbol!(node::Node{T,A}, symbol) where {T<:MutableNodeMTG,A} = setfield!(node_mtg(node), :symbol, symbol) +symbol!(node::Node{T,A}, symbol) where {T<:MutableNodeMTG,A} = setfield!(node_mtg(node), :symbol, to_mtg_symbol(symbol)) function symbol!(node::Node{T,A}, new_symbol) where {T<:NodeMTG,A} current_node_mtg = node_mtg(node) node_mtg!(node, NodeMTG(current_node_mtg.link, new_symbol, current_node_mtg.index, current_node_mtg.scale)) @@ -326,7 +326,7 @@ end Set the link of the MTG encoding of the node. It can be one of "/", "<", or "+". """ -link!(node::Node{T,A}, new_link) where {T<:MutableNodeMTG,A} = setfield!(node_mtg(node), :link, new_link) +link!(node::Node{T,A}, new_link) where {T<:MutableNodeMTG,A} = setfield!(node_mtg(node), :link, to_mtg_link(new_link)) function link!(node::Node{T,A}, new_link) where {T<:NodeMTG,A} current_node_mtg = node_mtg(node) node_mtg!(node, NodeMTG(new_link, current_node_mtg.symbol, current_node_mtg.index, current_node_mtg.scale)) diff --git a/src/write_mtg/write_mtg.jl b/src/write_mtg/write_mtg.jl index 3736256..a9029ca 100644 --- a/src/write_mtg/write_mtg.jl +++ b/src/write_mtg/write_mtg.jl @@ -169,7 +169,7 @@ function get_node_printing!(node, lead, ref, print_node, node_lead=0, node_ref=" push!(ref, node_ref) index = node_mtg(node).index == -9999 ? "" : string(node_mtg(node).index) - push!(print_node, node_mtg(node).link * symbol(node) * index) + push!(print_node, String(link(node)) * String(symbol(node)) * index) if !isleaf(node) chnodes = children(node) @@ -187,4 +187,4 @@ function get_node_printing!(node, lead, ref, print_node, node_lead=0, node_ref=" get_node_printing!(chnode, lead, ref, print_node, chnode_lead, node_ref) end end -end \ No newline at end of file +end diff --git a/test/test-caching.jl b/test/test-caching.jl index 9f4ac22..786f26d 100644 --- a/test/test-caching.jl +++ b/test/test-caching.jl @@ -4,21 +4,21 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files", mtg = read_mtg(file, Dict) # Cache all leaf nodes: - cache_nodes!(mtg, symbol="Leaf") + cache_nodes!(mtg, symbol=:Leaf) # Cached nodes are stored in the traversal_cache field of the mtg (here, the two leaves): @test MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_78a6583f9e4f630383e9f8bdcd9d1bc5d1a6e540"] == [get_node(mtg, 5), get_node(mtg, 7)] # cache_nodes!(mtg, is_leaf) - @test traverse(mtg, symbol, symbol="Leaf") == ["Leaf", "Leaf"] + @test traverse(mtg, symbol, symbol=:Leaf) == [:Leaf, :Leaf] # Modifying the mtg via the cache: - traverse!(mtg, symbol="Leaf") do x + traverse!(mtg, symbol=:Leaf) do x x[:x] = node_id(x) end @test [get_node(mtg, 5)[:x], get_node(mtg, 7)[:x]] == [5, 7] # Cache based on another symbol: - cache_nodes!(mtg, symbol="Internode") + cache_nodes!(mtg, symbol=:Internode) @test length(MultiScaleTreeGraph.node_traversal_cache(mtg)) == 2 @test length(MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_8b5413ba4d893f432a65c83e5bd53e5839e22a5d"]) == 2 @test MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_8b5413ba4d893f432a65c83e5bd53e5839e22a5d"] == [get_node(mtg, 4), get_node(mtg, 6)] @@ -29,12 +29,12 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files", @test length(MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_ab6319555fc952f43d7d80401e3f1f6124fd6644"]) == length(mtg) # Cache with 2 filters: - cache_nodes!(mtg, symbol="Internode", link="<") + cache_nodes!(mtg, symbol=:Internode, link=:<) @test length(MultiScaleTreeGraph.node_traversal_cache(mtg)) == 4 @test length(MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_ede6d4d594437acaca56712d385c03e014ff1b4b"]) == 1 @test MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_ede6d4d594437acaca56712d385c03e014ff1b4b"] == [get_node(mtg, 6)] - traverse!(mtg, symbol="Internode", link="<") do x + traverse!(mtg, symbol=:Internode, link=:<) do x x[:x] = node_id(x) + 2 end @test get_node(mtg, 6)[:x] == (get_node(mtg, 6) |> node_id) + 2 @@ -49,7 +49,7 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files", # Manually put a node in the cache and use it for computation: # Carefull, this is just for testing purpose, it is not recommended to do this in a real case. no_filter_cache_name = MultiScaleTreeGraph.cache_name(nothing, nothing, nothing, true, nothing) - MultiScaleTreeGraph.node_traversal_cache(mtg)[no_filter_cache_name] = [MultiScaleTreeGraph.Node(MutableNodeMTG("/", "Test", 1, 0), Dict{Symbol,Any}(:a => 1))] + MultiScaleTreeGraph.node_traversal_cache(mtg)[no_filter_cache_name] = [MultiScaleTreeGraph.Node(MutableNodeMTG(:/, :Test, 1, 0), Dict{Symbol,Any}(:a => 1))] # Test if the cache is used: traverse!(mtg) do x diff --git a/test/test-delete-prune.jl b/test/test-delete-prune.jl index b92cf89..a9c7788 100644 --- a/test/test-delete-prune.jl +++ b/test/test-delete-prune.jl @@ -12,7 +12,7 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files", # There is a warning because we don't know how to properly link the leaf (node 5) to its new parent. # Furthermore, the link of the second child (node 6) was modified from following to decomposing: - @test link(get_node(mtg, 6)) == "/" + @test link(get_node(mtg, 6)) == :/ # Delete a root node: mtg = read_mtg(file) @@ -27,14 +27,14 @@ end mtg = delete_node!(mtg) @test length(mtg) == length_start - 1 - @test symbol(mtg) == "Individual" + @test symbol(mtg) == :Individual @test scale(mtg) == 1 @test index(mtg) == 0 - @test link(mtg) == "/" + @test link(mtg) == :/ # Deleting the root node with several children should raise an error: mtg = read_mtg(file) - Node(mtg, MutableNodeMTG("/", "Branch", 1, 2)) + Node(mtg, MutableNodeMTG(:/, :Branch, 1, 2)) length_start = length(mtg) VERSION >= v"1.7" && @test_throws "Can't delete the root node if it has several children" delete_node!(mtg) end @@ -43,12 +43,12 @@ end mtg = read_mtg(file) delete_node!(get_node(mtg, 4), child_link_fun=link) # The link of the children didn't change: - @test link(get_node(mtg, 6)) == "<" + @test link(get_node(mtg, 6)) == :< - delete_node!(get_node(mtg, 3), child_link_fun=node -> "+") + delete_node!(get_node(mtg, 3), child_link_fun=node -> :+) # Both children links changes to branching: - @test link(get_node(mtg, 5)) == "+" - @test link(get_node(mtg, 6)) == "+" + @test link(get_node(mtg, 5)) == :+ + @test link(get_node(mtg, 6)) == :+ end @testset "delete_nodes!" begin @@ -60,7 +60,7 @@ end # Delete the leaves: mtg = read_mtg(file) - delete_nodes!(mtg, symbol="Leaf") + delete_nodes!(mtg, symbol=:Leaf) @test length(mtg) === length_start - 2 # Delete with a function, here we delete all nodes that have a parent with Length < 0.2: @@ -79,7 +79,7 @@ end # Delete multiple root nodes mtg = read_mtg(file) - new_mtg = delete_nodes!(mtg, filter_fun = node -> node_mtg(node).scale != 3) + new_mtg = delete_nodes!(mtg, filter_fun=node -> node_mtg(node).scale != 3) @test length(new_mtg) == 4 end diff --git a/test/test-descendants.jl b/test/test-descendants.jl index 262763c..ba89049 100644 --- a/test/test-descendants.jl +++ b/test/test-descendants.jl @@ -10,22 +10,22 @@ @test d[1] === width_all[1] d_typed = descendants(mtg, :Width, type=Union{Nothing,Float64}) @test typeof(d_typed) == Vector{Union{Nothing,Float64}} - @test descendants(mtg, :Width, symbol=("Leaf", "Internode")) == width_all[3:end] + @test descendants(mtg, :Width, symbol=(:Leaf, :Internode)) == width_all[3:end] mtg2 = mtg[1][1][1][2] - @test descendants(mtg2, :Width, symbol="Leaf")[1] == width_all[end] + @test descendants(mtg2, :Width, symbol=:Leaf)[1] == width_all[end] - @test descendants(mtg2, :Width, symbol=("Leaf", "Internode"), self=true) == + @test descendants(mtg2, :Width, symbol=(:Leaf, :Internode), self=true) == width_all[end-1:end] out_vals = Union{Nothing,Float64}[] @test descendants!(out_vals, mtg, :Width) == width_all - @test descendants!(out_vals, mtg2, :Width, symbol=("Leaf", "Internode"), self=true) == + @test descendants!(out_vals, mtg2, :Width, symbol=(:Leaf, :Internode), self=true) == width_all[end-1:end] # Using the mutating version: @test descendants!(mtg, :Width) == descendants(mtg, :Width) - @test descendants!(mtg2, :Width, symbol=("Leaf", "Internode"), self=true) == + @test descendants!(mtg2, :Width, symbol=(:Leaf, :Internode), self=true) == width_all[end-1:end] clean_cache!(mtg) diff --git a/test/test-insert_node.jl b/test/test-insert_node.jl index 9a5415e..d6c496e 100644 --- a/test/test-insert_node.jl +++ b/test/test-insert_node.jl @@ -1,7 +1,7 @@ @testset "test insert_parent!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MultiScaleTreeGraph.MutableNodeMTG("/", "Shoot", 0, 1) + template = MultiScaleTreeGraph.MutableNodeMTG(:/, :Shoot, 0, 1) max_node_id = [max_id(mtg)] length_before = length(mtg) @@ -66,7 +66,7 @@ end @testset "test insert_parents!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MutableNodeMTG("/", "Shoot", 0, 1) + template = MutableNodeMTG(:/, :Shoot, 0, 1) length_before = length(mtg) insert_parents!( mtg, @@ -84,7 +84,7 @@ end @testset "test insert_child!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MultiScaleTreeGraph.MutableNodeMTG("/", "Shoot", 0, 1) + template = MultiScaleTreeGraph.MutableNodeMTG(:/, :Shoot, 0, 1) max_node_id = [max_id(mtg)] length_before = length(mtg) @@ -100,9 +100,9 @@ end @testset "test insert_children!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MutableNodeMTG("/", "Rachis", 0, 4) + template = MutableNodeMTG(:/, :Rachis, 0, 4) length_before = length(mtg) - insert_children!(mtg, template, symbol="Leaf") + insert_children!(mtg, template, symbol=:Leaf) @test length(mtg) == length_before + 2 @@ -113,7 +113,7 @@ end @testset "test insert_generation!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MultiScaleTreeGraph.MutableNodeMTG("/", "Shoot", 0, 1) + template = MultiScaleTreeGraph.MutableNodeMTG(:/, :Shoot, 0, 1) max_node_id = [max_id(mtg)] length_before = length(mtg) @@ -132,9 +132,9 @@ end @testset "test insert_generations!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MutableNodeMTG("/", "Rachis", 0, 4) + template = MutableNodeMTG(:/, :Rachis, 0, 4) length_before = length(mtg) - insert_generations!(mtg, template, symbol="Internode") + insert_generations!(mtg, template, symbol=:Internode) @test length(mtg) == length_before + 2 @@ -145,7 +145,7 @@ end @testset "test insert_sibling!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MultiScaleTreeGraph.MutableNodeMTG("/", "Individual", 0, 1) + template = MultiScaleTreeGraph.MutableNodeMTG(:/, :Individual, 0, 1) max_node_id = [max_id(mtg)] length_before = length(mtg) @@ -160,10 +160,10 @@ end @testset "test insert_siblings!" begin mtg = read_mtg("files/simple_plant.mtg") - template = MutableNodeMTG("+", "Leaf", 0, 3) + template = MutableNodeMTG(:+, :Leaf, 0, 3) length_before = length(mtg) # Add a new leaf at each leaf node: - insert_siblings!(mtg, template, symbol="Leaf") + insert_siblings!(mtg, template, symbol=:Leaf) @test length(mtg) == length_before + 2 diff --git a/test/test-nodes.jl b/test/test-nodes.jl index bfd7aa8..1949917 100644 --- a/test/test-nodes.jl +++ b/test/test-nodes.jl @@ -2,52 +2,65 @@ @testset "NodeMTG and MutableNodeMTG" begin # Test NodeMTG constructor with all arguments - node_mtg = MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 2) - @test node_mtg.link == "/" - @test node_mtg.symbol == "Plant" + node_mtg = MultiScaleTreeGraph.NodeMTG(:/, :Plant, 1, 2) + @test node_mtg.link == :/ + @test node_mtg.symbol == :Plant @test node_mtg.index == 1 @test node_mtg.scale == 2 + # Symbol inputs should also be accepted and exposed as strings for compatibility + node_mtg_sym = MultiScaleTreeGraph.NodeMTG(:/, :Plant, 1, 2) + @test node_mtg_sym.link == :/ + @test node_mtg_sym.symbol == :Plant + # Test NodeMTG constructor with nothing as index - node_mtg_nothing = MultiScaleTreeGraph.NodeMTG("/", "Internode", nothing, 3) - @test node_mtg_nothing.link == "/" - @test node_mtg_nothing.symbol == "Internode" + node_mtg_nothing = MultiScaleTreeGraph.NodeMTG(:/, :Internode, nothing, 3) + @test node_mtg_nothing.link == :/ + @test node_mtg_nothing.symbol == :Internode @test node_mtg_nothing.index == -9999 @test node_mtg_nothing.scale == 3 # Test MutableNodeMTG constructor with all arguments - mutable_node_mtg = MultiScaleTreeGraph.MutableNodeMTG("+", "Leaf", 2, 4) - @test mutable_node_mtg.link == "+" - @test mutable_node_mtg.symbol == "Leaf" + mutable_node_mtg = MultiScaleTreeGraph.MutableNodeMTG(:+, :Leaf, 2, 4) + @test mutable_node_mtg.link == :+ + @test mutable_node_mtg.symbol == :Leaf @test mutable_node_mtg.index == 2 @test mutable_node_mtg.scale == 4 # Test MutableNodeMTG constructor with nothing as index - mutable_node_mtg_nothing = MultiScaleTreeGraph.MutableNodeMTG("<", "Apex", nothing, 5) - @test mutable_node_mtg_nothing.link == "<" - @test mutable_node_mtg_nothing.symbol == "Apex" + mutable_node_mtg_nothing = MultiScaleTreeGraph.MutableNodeMTG(:<, :Apex, nothing, 5) + @test mutable_node_mtg_nothing.link == :< + @test mutable_node_mtg_nothing.symbol == :Apex @test mutable_node_mtg_nothing.index == -9999 @test mutable_node_mtg_nothing.scale == 5 + mutable_node_mtg_sym = MultiScaleTreeGraph.MutableNodeMTG(:+, :Leaf, 2, 4) + @test mutable_node_mtg_sym.link == :+ + @test mutable_node_mtg_sym.symbol == :Leaf + # Test mutability of MutableNodeMTG - mutable_node_mtg.link = "<" - mutable_node_mtg.symbol = "Flower" + mutable_node_mtg.link = :< + mutable_node_mtg.symbol = :Flower mutable_node_mtg.index = 3 mutable_node_mtg.scale = 6 - @test mutable_node_mtg.link == "<" - @test mutable_node_mtg.symbol == "Flower" + @test mutable_node_mtg.link == :< + @test mutable_node_mtg.symbol == :Flower + mutable_node_mtg.link = :+ + mutable_node_mtg.symbol = :Leaf + @test mutable_node_mtg.link == :+ + @test mutable_node_mtg.symbol == :Leaf @test mutable_node_mtg.index == 3 @test mutable_node_mtg.scale == 6 # Test assertions - @test_throws AssertionError MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, -1) # scale < 0 - @test_throws AssertionError MultiScaleTreeGraph.NodeMTG("invalid", "Plant", 1, 1) # invalid link - @test_throws AssertionError MultiScaleTreeGraph.MutableNodeMTG("/", "Plant", 1, -1) # scale < 0 - @test_throws AssertionError MultiScaleTreeGraph.MutableNodeMTG("invalid", "Plant", 1, 1) # invalid link + @test_throws AssertionError MultiScaleTreeGraph.NodeMTG(:/, :Plant, 1, -1) # scale < 0 + @test_throws AssertionError MultiScaleTreeGraph.NodeMTG(:invalid, :Plant, 1, 1) # invalid link + @test_throws AssertionError MultiScaleTreeGraph.MutableNodeMTG(:/, :Plant, 1, -1) # scale < 0 + @test_throws AssertionError MultiScaleTreeGraph.MutableNodeMTG(:invalid, :Plant, 1, 1) # invalid link end @testset "Create node" begin - mtg_code = MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1) + mtg_code = MultiScaleTreeGraph.NodeMTG(:/, :Plant, 1, 1) mtg = MultiScaleTreeGraph.Node(mtg_code) @test get_attributes(mtg) == [] @test node_attributes(mtg) == Dict{Symbol,Any}() @@ -57,15 +70,15 @@ end end @testset "Create node" begin - mtg_code = MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1) + mtg_code = MultiScaleTreeGraph.NodeMTG(:/, :Plant, 1, 1) mtg = MultiScaleTreeGraph.Node(mtg_code) internode = MultiScaleTreeGraph.Node( mtg, - MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2) + MultiScaleTreeGraph.NodeMTG(:/, :Internode, 1, 2) ) @test parent(internode) == mtg @test node_id(internode) == 2 - @test node_mtg(internode) == MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2) + @test node_mtg(internode) == MultiScaleTreeGraph.NodeMTG(:/, :Internode, 1, 2) @test node_attributes(internode) == Dict{Symbol,Any}() @test children(mtg) == [internode] end @@ -105,6 +118,5 @@ end @testset "Adding a child with a different MTG encoding type" begin mtg = read_mtg(file, Dict, MutableNodeMTG) - VERSION >= v"1.7" && @test_throws "The parent node has an MTG encoding of type `MutableNodeMTG`, but the MTG encoding you provide is of type `NodeMTG`, please make sure they are the same." Node(mtg, NodeMTG("/", "Branch", 1, 2)) + VERSION >= v"1.7" && @test_throws "The parent node has an MTG encoding of type `MutableNodeMTG`, but the MTG encoding you provide is of type `NodeMTG`, please make sure they are the same." Node(mtg, NodeMTG(:/, :Branch, 1, 2)) end - diff --git a/test/test-read_mtg.jl b/test/test-read_mtg.jl index b9d7d76..a02ce9b 100644 --- a/test/test-read_mtg.jl +++ b/test/test-read_mtg.jl @@ -58,7 +58,7 @@ end @testset "test mtg mutation" begin @test (node_attributes(mtg)[:scales] .= [0, 1, 2, 3, 4]) == [0, 1, 2, 3, 4] - @test MultiScaleTreeGraph.node_mtg!(mtg, MultiScaleTreeGraph.NodeMTG("<", "Leaf", 2, 0)) == MultiScaleTreeGraph.NodeMTG("<", "Leaf", 2, 0) + @test MultiScaleTreeGraph.node_mtg!(mtg, MultiScaleTreeGraph.NodeMTG(:<, :Leaf, 2, 0)) == MultiScaleTreeGraph.NodeMTG(:<, :Leaf, 2, 0) reparent!(mtg[1], nothing) @test parent(mtg[1]) === nothing node2 = mtg[1] @@ -72,7 +72,7 @@ end @test length(mtg) == 7 @test typeof(mtg) == Node{NodeMTG,NamedTuple} @test node_id(mtg) == 1 - @test node_mtg(mtg) == MultiScaleTreeGraph.NodeMTG("/", "Scene", 0, 0) + @test node_mtg(mtg) == MultiScaleTreeGraph.NodeMTG(:/, :Scene, 0, 0) @test typeof(children(mtg)) == Vector{Node{NodeMTG,NamedTuple}} @test node_id(mtg[1]) == 2 @test parent(mtg[1]) === mtg @@ -85,7 +85,7 @@ end @test node_id(mtg) == 1 @test node_attributes(mtg) == Dict(:symbols => ["Scene", "Individual", "Axis", "Internode", "Leaf"], :scales => [0, 1, 2, 3, 3], :description => mtg[:description]) - @test node_mtg(mtg) == MultiScaleTreeGraph.NodeMTG("/", "Scene", 0, 0) + @test node_mtg(mtg) == MultiScaleTreeGraph.NodeMTG(:/, :Scene, 0, 0) @test typeof(children(mtg)) == Vector{Node{NodeMTG,Dict{Symbol,Any}}} @test node_id(mtg[1]) == 2 @test parent(mtg[1]) === mtg @@ -94,7 +94,7 @@ end @testset "test mtg with Dict: mutation" begin mtg = read_mtg("files/simple_plant.mtg", Dict, NodeMTG) @test (node_attributes(mtg)[:scales] = [0, 1, 2, 3, 4]) == [0, 1, 2, 3, 4] - @test MultiScaleTreeGraph.node_mtg!(mtg, MultiScaleTreeGraph.NodeMTG("<", "Leaf", 2, 0)) == MultiScaleTreeGraph.NodeMTG("<", "Leaf", 2, 0) + @test MultiScaleTreeGraph.node_mtg!(mtg, MultiScaleTreeGraph.NodeMTG(:<, :Leaf, 2, 0)) == MultiScaleTreeGraph.NodeMTG(:<, :Leaf, 2, 0) reparent!(mtg[1], nothing) @test parent(mtg[1]) === nothing node2 = mtg[1] diff --git a/test/test-summary.jl b/test/test-summary.jl index 27525ab..9fe39bb 100644 --- a/test/test-summary.jl +++ b/test/test-summary.jl @@ -2,14 +2,14 @@ mtg = read_mtg("files/simple_plant.mtg"); @testset "getting scales" begin @test scales(mtg) == [0, 1, 2, 3] - @test symbols(mtg) == components(mtg) == ["Scene", "Individual", "Axis", "Internode", "Leaf"] + @test symbols(mtg) == components(mtg) == [:Scene, :Individual, :Axis, :Internode, :Leaf] end @testset "test classes" begin classes = get_classes(mtg) @test typeof(classes) == DataFrame @test size(classes) == (5, 5) - @test String.(classes.SYMBOL) == ["Scene", "Individual", "Axis", "Internode", "Leaf"] + @test classes.SYMBOL == [:Scene, :Individual, :Axis, :Internode, :Leaf] @test classes.SCALE == [0, 1, 2, 3, 3] @test classes.DECOMPOSITION == ["FREE" for i = 1:5] @test classes.INDEXATION == ["FREE" for i = 1:5] diff --git a/test/test-traverse.jl b/test/test-traverse.jl index 709c919..886954d 100644 --- a/test/test-traverse.jl +++ b/test/test-traverse.jl @@ -27,24 +27,29 @@ end mtg = read_mtg("files/simple_plant.mtg", Dict) # Add a new attributes, x based on node field, y based on x and z using a function: @test traverse(mtg, x -> x, symbol="Internode") == [get_node(mtg, 4), get_node(mtg, 6)] + @test traverse(mtg, x -> x, symbol=:Internode) == [get_node(mtg, 4), get_node(mtg, 6)] @test traverse(mtg, x -> x, scale=3) == [get_node(mtg, 4), get_node(mtg, 5), get_node(mtg, 6), get_node(mtg, 7)] @test traverse(mtg, x -> x, scale=3) == traverse(mtg, x -> x, symbol=["Internode", "Leaf"]) + @test traverse(mtg, x -> x, scale=3) == traverse(mtg, x -> x, symbol=[:Internode, :Leaf]) @test traverse(mtg, x -> x, link="+") == [get_node(mtg, 5), get_node(mtg, 7)] + @test traverse(mtg, x -> x, link=:+) == [get_node(mtg, 5), get_node(mtg, 7)] @test traverse(mtg, x -> x, link="<") == [get_node(mtg, 6)] + @test traverse(mtg, x -> x, link=:<) == [get_node(mtg, 6)] @test traverse(mtg, x -> x, link=["<", "+"]) == [get_node(mtg, 5), get_node(mtg, 6), get_node(mtg, 7)] + @test traverse(mtg, x -> x, link=[:<, :+]) == [get_node(mtg, 5), get_node(mtg, 6), get_node(mtg, 7)] @test traverse(mtg, x -> x, filter_fun=node -> node[:Length] !== nothing && node[:Length] == 0.1) == [get_node(mtg, 4), get_node(mtg, 6)] - @test traverse(mtg, x -> x, symbol="Internode", filter_fun=node -> node[:Length] !== nothing) == [get_node(mtg, 4), get_node(mtg, 6)] - @test traverse(mtg, x -> x, filter_fun=node -> symbol(node) != "Internode", all=false) == [get_node(mtg, i) for i in 1:3] - @test traverse(mtg, x -> x, symbol="Internode", all=false) == Any[] # No internode in the first level, all=false -> iteration stops before the first node + @test traverse(mtg, x -> x, symbol=:Internode, filter_fun=node -> node[:Length] !== nothing) == [get_node(mtg, 4), get_node(mtg, 6)] + @test traverse(mtg, x -> x, filter_fun=node -> symbol(node) != :Internode, all=false) == [get_node(mtg, i) for i in 1:3] + @test traverse(mtg, x -> x, symbol=:Internode, all=false) == Any[] # No internode in the first level, all=false -> iteration stops before the first node @test traverse(mtg, node -> node[:Length]) == Any[nothing, nothing, nothing, 0.1, 0.2, 0.1, 0.2] @test traverse(mtg, node -> node[:Length], type=Union{Nothing,Float64}) == Union{Nothing,Float64}[nothing, nothing, nothing, 0.1, 0.2, 0.1, 0.2] end @testset "traverse deep no-filter path" begin - root = Node(1, NodeMTG("/", "Plant", 1, 1)) + root = Node(1, NodeMTG(:/, :Plant, 1, 1)) current = root for i in 1:5000 - current = Node(i + 1, current, NodeMTG("<", "Segment", i, 2)) + current = Node(i + 1, current, NodeMTG(:<, :Segment, i, 2)) end out = traverse(root, node -> node_id(node), type=Int) diff --git a/test/test-write_mtg.jl b/test/test-write_mtg.jl index ea88b9f..660fd8f 100644 --- a/test/test-write_mtg.jl +++ b/test/test-write_mtg.jl @@ -73,8 +73,8 @@ mtg = read_mtg(file) @test length(mtg) == 35 @test length(children(mtg[1][1])) == 2 - @test children(mtg[1][1])[1] |> symbol == "N" - @test children(mtg[1][1])[2] |> symbol == "GU" + @test children(mtg[1][1])[1] |> symbol == :N + @test children(mtg[1][1])[2] |> symbol == :GU mtg2 = mktemp() do f, io write_mtg(f, mtg)