Skip to content

Commit f0186af

Browse files
authored
BREAKING: Merge pull request #81 from VEZY/symbols-for-links-and-symbol-names
Use symbols for mtg link and symbol. Breaking as now `symbol(node)` and `link(node)` return a symbol instead of a `String`
2 parents e9ab531 + dccd05a commit f0186af

30 files changed

Lines changed: 308 additions & 167 deletions

docs/src/tutorials/2.descendants_ancestors_filters.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,10 @@ descendants(mtg, :Length, scale = 3)
133133

134134
### Filter by symbol
135135

136-
If we need only the leaves, we would filter by their symbol (*i.e.* "Leaf"):
136+
If we need only the leaves, we would filter by their symbol (*i.e.* :Leaf):
137137

138138
```@example usepkg
139-
descendants(mtg, :Length, symbol = "Leaf")
139+
descendants(mtg, :Length, symbol = :Leaf)
140140
```
141141

142142
### Filter by anything
@@ -156,14 +156,14 @@ descendants(mtg, :Length, filter_fun = x -> x[:Width] === nothing ? false : x[:W
156156
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:
157157

158158
```@example usepkg
159-
descendants(mtg, :Length, filter_fun = node -> !isroot(node) && symbol(parent(node)) == "Internode" && link(parent(node)) == "<")
159+
descendants(mtg, :Length, filter_fun = node -> !isroot(node) && symbol(parent(node)) == :Internode && link(parent(node)) == :<)
160160
```
161161

162162
In this example it returns only one value, because there is only one node that corresponds to this criteria: The Leaf with id 7.
163163

164164
We could apply the same kind of filtering on the node's children, or any combination of topological information and attributes.
165165

166-
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 "<".
166+
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 :<.
167167

168168
### Filter helpers
169169

docs/src/tutorials/3.transform_mtg.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ transform!(mtg, node -> isleaf(node) ? println(node_id(node)," is a leaf") : not
213213
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":
214214

215215
```@example usepkg
216-
transform!(mtg, node -> symbol!(node, "I"), symbol = "Internode")
216+
transform!(mtg, node -> symbol!(node, :I), symbol = :Internode)
217217
218218
mtg
219219
```
@@ -255,7 +255,7 @@ DataFrame(mtg_select)
255255
[`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`:
256256

257257
```@example usepkg
258-
traverse!(mtg, node -> symbol!(node, "L"), symbol = "Leaf")
258+
traverse!(mtg, node -> symbol!(node, :L), symbol = :Leaf)
259259
260260
mtg
261261
```
@@ -275,7 +275,7 @@ end
275275
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:
276276

277277
```@example usepkg
278-
@mutate_mtg!(mtg, volume = π * 2 * node.Length, symbol = "I")
278+
@mutate_mtg!(mtg, volume = π * 2 * node.Length, symbol = :I)
279279
```
280280

281281
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.`.

docs/src/tutorials/6.add_remove_nodes.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Those functions use a `NodeMTG` (or `MutableNodeMTG`), and automatically:
100100
```@example usepkg
101101
mtg_2 = deepcopy(mtg)
102102
103-
insert_parent!(mtg_2, NodeMTG("/", "Scene", 0, 0))
103+
insert_parent!(mtg_2, NodeMTG(:/, :Scene, 0, 0))
104104
105105
mtg_2 = get_root(mtg_2)
106106
```
@@ -114,7 +114,7 @@ insert_parent!(
114114
mtg_2,
115115
node -> (
116116
link = link(node),
117-
symbol = "Scene",
117+
symbol = :Scene,
118118
index = index(node),
119119
scale = scale(node) - 1
120120
)
@@ -147,7 +147,7 @@ mtg_2 = deepcopy(mtg)
147147
148148
insert_child!(
149149
mtg_2,
150-
NodeMTG("/", "Axis", 0, 2),
150+
NodeMTG(:/, :Axis, 0, 2),
151151
node -> Dict{Symbol, Any}(:length => 2, :area => 0.1)
152152
)
153153
@@ -161,7 +161,7 @@ mtg_2 = deepcopy(mtg)
161161
162162
insert_child!(
163163
mtg_2,
164-
NodeMTG("/", "Axis", 0, 2),
164+
NodeMTG(:/, :Axis, 0, 2),
165165
node -> Dict{Symbol, Any}(:total_length => sum(descendants(node, :length, ignore_nothing = true)))
166166
)
167167
@@ -265,23 +265,23 @@ For example if we need to insert new Flower nodes as parents of each Leaf, we wo
265265

266266
```@example usepkg
267267
mtg_4 = deepcopy(mtg)
268-
template = MutableNodeMTG("+", "Flower", 0, 2)
269-
insert_parents!(mtg_4, template, symbol = "Leaf")
268+
template = MutableNodeMTG(:+, :Flower, 0, 2)
269+
insert_parents!(mtg_4, template, symbol = :Leaf)
270270
```
271271

272272
Similarly, we can add a new child to leaves using [`insert_children!`](@ref):
273273

274274
```@example usepkg
275-
template = MutableNodeMTG("/", "Leaflet", 0, 3)
276-
insert_children!(mtg_4, template, symbol = "Leaf")
275+
template = MutableNodeMTG(:/, :Leaflet, 0, 3)
276+
insert_children!(mtg_4, template, symbol = :Leaf)
277277
```
278278

279279
Usually, the flower is positioned as a sibling of the leaf though. To do so, we can use [`insert_siblings!`](@ref):
280280

281281
```@example usepkg
282282
mtg_5 = deepcopy(mtg)
283-
template = MutableNodeMTG("+", "Flower", 0, 2)
284-
insert_siblings!(mtg_5, template, symbol = "Leaf")
283+
template = MutableNodeMTG(:+, :Flower, 0, 2)
284+
insert_siblings!(mtg_5, template, symbol = :Leaf)
285285
```
286286

287287
### 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
291291
```@example usepkg
292292
insert_children!(
293293
mtg_5,
294-
node -> if node_id(node) == 3 MutableNodeMTG("/", "Spear", 0, 3) else MutableNodeMTG("/", "Leaflet", 0, 3) end,
295-
symbol = "Leaf"
294+
node -> if node_id(node) == 3 MutableNodeMTG(:/, :Spear, 0, 3) else MutableNodeMTG(:/, :Leaflet, 0, 3) end,
295+
symbol = :Leaf
296296
)
297297
```
298298

@@ -303,9 +303,9 @@ The same is true for the attributes. We can provide them as is:
303303
```@example usepkg
304304
insert_siblings!(
305305
mtg_5,
306-
MutableNodeMTG("+", "Leaf", 0, 2),
306+
MutableNodeMTG(:+, :Leaf, 0, 2),
307307
Dict{Symbol, Any}(:area => 0.1),
308-
symbol = "Leaf"
308+
symbol = :Leaf
309309
)
310310
```
311311

@@ -314,9 +314,9 @@ Or compute them based on the node on which we insert the new nodes. For example
314314
```@example usepkg
315315
insert_siblings!(
316316
mtg_5,
317-
MutableNodeMTG("+", "Leaf", 0, 2),
317+
MutableNodeMTG(:+, :Leaf, 0, 2),
318318
node -> node[:area] === nothing ? nothing : Dict{Symbol, Any}(:area => node[:area] * 2),
319-
symbol = "Leaf"
319+
symbol = :Leaf
320320
)
321321
```
322322

docs/src/tutorials/7.performance_considerations.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ The MTG encoding is the type used to store the MTG information about the node, *
2222

2323
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.
2424

25+
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:
26+
27+
```julia
28+
NodeMTG(:/, :Internode, 1, 2)
29+
traverse(mtg, x -> x, symbol=:Internode, link=:<)
30+
```
31+
2532
### Traversal: node caching
2633

2734
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
3441
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:
3542

3643
```julia
37-
cache_nodes!(mtg, symbol = "Leaf")
44+
cache_nodes!(mtg, symbol = :Leaf)
3845
```
3946

40-
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.
47+
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.
4148

4249
!!! tip
4350
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.

src/compute_MTG/ancestors.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Make it a `Symbol` for faster computation time.
1515
## Keyword Arguments
1616
1717
- `scale = nothing`: The scale to filter-in (i.e. to keep). Usually a Tuple-alike of integers.
18-
- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Strings.
18+
- `symbol = nothing`: The symbol to filter-in. Usually a Tuple-alike of Symbols.
1919
- `link = nothing`: The link with the previous node to filter-in. Usually a Tuple-alike of Char.
2020
- `all::Bool = true`: Return all filtered-in nodes (`true`), or stop at the first node that
2121
is filtered out (`false`).
@@ -56,8 +56,8 @@ ancestors(leaf_node, :XX, scale = 1, type = Float64)
5656
ancestors(leaf_node, :Length, scale = 3, type = Float64)
5757
5858
# Filter by symbol:
59-
ancestors(leaf_node, :Length, symbol = "Internode")
60-
ancestors(leaf_node, :Length, symbol = ("Axis","Internode"))
59+
ancestors(leaf_node, :Length, symbol = :Internode)
60+
ancestors(leaf_node, :Length, symbol = (:Axis,:Internode))
6161
```
6262
"""
6363
function ancestors(
@@ -71,6 +71,8 @@ function ancestors(
7171
recursivity_level=-1,
7272
ignore_nothing=false,
7373
type::Union{Union,DataType}=Any)
74+
symbol = normalize_symbol_filter(symbol)
75+
link = normalize_link_filter(link)
7476

7577
# Check the filters once, and then compute the ancestors recursively using `ancestors_`
7678
check_filters(node, scale=scale, symbol=symbol, link=link)
@@ -146,6 +148,8 @@ function ancestors(
146148
filter_fun=nothing,
147149
recursivity_level=-1
148150
)
151+
symbol = normalize_symbol_filter(symbol)
152+
link = normalize_link_filter(link)
149153

150154
# Check the filters once, and then compute the ancestors recursively using `ancestors_`
151155
check_filters(node, scale=scale, symbol=symbol, link=link)
@@ -219,6 +223,8 @@ function ancestors!(
219223
ignore_nothing=false,
220224
type::Union{Union,DataType}=Any,
221225
)
226+
symbol = normalize_symbol_filter(symbol)
227+
link = normalize_link_filter(link)
222228
check_filters(node, scale=scale, symbol=symbol, link=link)
223229
filter_fun_ = filter_fun_nothing(filter_fun, ignore_nothing, key)
224230
use_no_filter = no_node_filters(scale, symbol, link, filter_fun_)
@@ -251,6 +257,8 @@ function ancestors!(
251257
filter_fun=nothing,
252258
recursivity_level=-1,
253259
)
260+
symbol = normalize_symbol_filter(symbol)
261+
link = normalize_link_filter(link)
254262
check_filters(node, scale=scale, symbol=symbol, link=link)
255263
use_no_filter = no_node_filters(scale, symbol, link, filter_fun)
256264

src/compute_MTG/caching.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,18 @@ file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))), "test", "files",
4444
mtg = read_mtg(file, Dict)
4545
4646
# Cache all leaf nodes:
47-
cache_nodes!(mtg, symbol="Leaf")
47+
cache_nodes!(mtg, symbol=:Leaf)
4848
4949
# Cached nodes are stored in the traversal_cache field of the mtg (here, the two leaves):
5050
@test MultiScaleTreeGraph.node_traversal_cache(mtg)["_cache_c0bffb8cc8a9b075e40d26be9c2cac6349f2a790"] == [get_node(mtg, 5), get_node(mtg, 7)]
5151
5252
# Then you can use the cached nodes in a traversal:
53-
traverse(mtg, x -> symbol(x), symbol="Leaf") == ["Leaf", "Leaf"]
53+
traverse(mtg, x -> symbol(x), symbol=:Leaf) == [:Leaf, :Leaf]
5454
```
5555
"""
5656
function cache_nodes!(node; scale=nothing, symbol=nothing, link=nothing, filter_fun=nothing, all=true, overwrite=false)
57+
symbol = normalize_symbol_filter(symbol)
58+
link = normalize_link_filter(link)
5759
# The cache is already present:
5860
if length(node_traversal_cache(node)) != 0 && haskey(node_traversal_cache(node), cache_name(scale, symbol, link, all, filter_fun))
5961
if !overwrite
@@ -71,4 +73,4 @@ function cache_nodes!(node; scale=nothing, symbol=nothing, link=nothing, filter_
7173
)
7274

7375
return nothing
74-
end
76+
end

src/compute_MTG/check_filters.jl

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,33 @@ Check if the filters are consistant with the mtg onto which they are applied
88
```julia
99
check_filters(mtg, scale = 1)
1010
check_filters(mtg, scale = (1,2))
11-
check_filters(mtg, scale = (1,2), symbol = "Leaf", link = "<")
11+
check_filters(mtg, scale = (1,2), symbol = :Leaf, link = :<)
1212
```
1313
"""
1414
@inline no_node_filters(scale, symbol, link, filter_fun=nothing) =
1515
isnothing(scale) && isnothing(symbol) && isnothing(link) && isnothing(filter_fun)
1616

17+
@inline normalize_symbol_filter(filter::Nothing) = nothing
18+
@inline normalize_symbol_filter(filter::Symbol) = filter
19+
@inline normalize_symbol_filter(filter::AbstractString) = Symbol(filter)
20+
@inline normalize_symbol_filter(filter::Char) = Symbol(filter)
21+
@inline function normalize_symbol_filter(filter::T) where {T<:Union{Tuple,AbstractArray}}
22+
map(normalize_symbol_filter, filter)
23+
end
24+
25+
@inline normalize_link_filter(filter::Nothing) = nothing
26+
@inline normalize_link_filter(filter::Symbol) = filter
27+
@inline normalize_link_filter(filter::AbstractString) = Symbol(filter)
28+
@inline normalize_link_filter(filter::Char) = Symbol(filter)
29+
@inline function normalize_link_filter(filter::T) where {T<:Union{Tuple,AbstractArray}}
30+
map(normalize_link_filter, filter)
31+
end
32+
33+
@inline normalize_symbol_allowed(filters::Nothing) = nothing
34+
@inline function normalize_symbol_allowed(filters::T) where {T<:Union{Tuple,AbstractArray}}
35+
map(normalize_symbol_filter, filters)
36+
end
37+
1738
function check_filters(node::Node{N,A}; scale=nothing, symbol=nothing, link=nothing) where {N<:AbstractNodeMTG,A}
1839
no_node_filters(scale, symbol, link) && return nothing
1940

@@ -24,11 +45,11 @@ function check_filters(node::Node{N,A}; scale=nothing, symbol=nothing, link=noth
2445
end
2546

2647
if root_node[:symbols] !== nothing
27-
check_filter(N, :symbol, symbol, unique(root_node[:symbols]))
48+
check_filter(N, :symbol, normalize_symbol_filter(symbol), unique(normalize_symbol_allowed(root_node[:symbols])))
2849
end
2950

3051
if root_node[:link] !== nothing
31-
check_filter(N, :link, link, ("/", "<", "+"))
52+
check_filter(N, :link, normalize_link_filter(link), (:/, :<, :+))
3253
end
3354

3455
return nothing
@@ -37,7 +58,11 @@ end
3758
function check_filter(nodetype, type::Symbol, filter, filters)
3859
if !isnothing(filter)
3960
filter_type = fieldtype(nodetype, type)
40-
!(typeof(filter) <: filter_type) &&
61+
filter_ok = typeof(filter) <: filter_type
62+
if type == :symbol || type == :link
63+
filter_ok = filter_ok || typeof(filter) <: Union{Symbol,AbstractString,Char}
64+
end
65+
!filter_ok &&
4166
@warn "The $type argument should be of type $filter_type"
4267
if !(filter in filters)
4368
@warn "The $type argument should be one of: $filters, and you provided $filter."
@@ -58,9 +83,9 @@ end
5883
Is a node filtered in ? Returns `true` if the node is kept, `false` if it is filtered-out.
5984
"""
6085
@inline function is_filtered(node, mtg_scale, mtg_symbol, mtg_link, filter_fun)
61-
62-
link_keep = isnothing(mtg_link) || is_filtered(mtg_link, link(node))
63-
symbol_keep = isnothing(mtg_symbol) || is_filtered(mtg_symbol, symbol(node))
86+
node_mtg_ = node_mtg(node)
87+
link_keep = isnothing(mtg_link) || is_filtered(mtg_link, getfield(node_mtg_, :link))
88+
symbol_keep = isnothing(mtg_symbol) || is_filtered(mtg_symbol, getfield(node_mtg_, :symbol))
6489
scale_keep = isnothing(mtg_scale) || is_filtered(mtg_scale, scale(node))
6590
filter_fun_keep = isnothing(filter_fun) || filter_fun(node)
6691

@@ -75,8 +100,42 @@ end
75100
value in filter
76101
end
77102

78-
@inline function is_filtered(filter::String, value)
79-
value in (filter,)
103+
@inline function is_filtered(filter::AbstractString, value::Symbol)
104+
Symbol(filter) === value
105+
end
106+
107+
@inline function is_filtered(filter::AbstractString, value::AbstractString)
108+
filter == value
109+
end
110+
111+
@inline function is_filtered(filter::AbstractString, value)
112+
filter == value
113+
end
114+
115+
@inline function is_filtered(filter::Symbol, value::AbstractString)
116+
filter === Symbol(value)
117+
end
118+
119+
@inline function is_filtered(filter::Symbol, value::Symbol)
120+
filter === value
121+
end
122+
123+
@inline function is_filtered(filter::Symbol, value)
124+
filter === value
125+
end
126+
127+
@inline function is_filtered(filter::T, value::Symbol) where {T<:Union{Tuple,AbstractArray}}
128+
for f in filter
129+
is_filtered(f, value) && return true
130+
end
131+
return false
132+
end
133+
134+
@inline function is_filtered(filter::T, value::AbstractString) where {T<:Union{Tuple,AbstractArray}}
135+
for f in filter
136+
is_filtered(f, value) && return true
137+
end
138+
return false
80139
end
81140

82141
@inline function is_filtered(filter, value::T) where {T<:Union{Tuple,Array}}

0 commit comments

Comments
 (0)